Web-Ide mit aufgenommen
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
export default {
|
||||
moduleNameMapper: { "(.+)(?<!ohm)\\.js": "$1" },
|
||||
roots: ["src"],
|
||||
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||
transformIgnorePatterns: ["<rootDir>/node_modules/(?!@davidsouther/.*)"],
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@nand2tetris/simulator",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": "David Souther <davidsouther@gmail.com>",
|
||||
"license": "ISC",
|
||||
"homepage": "https://davidsouther.github.io/nand2tetris",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./build/*"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"build/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@davidsouther/jiffies": "^2.2.5",
|
||||
"@nand2tetris/projects": "file:../projects",
|
||||
"@nand2tetris/runner": "file:../runner",
|
||||
"@types/node": "^20.14.2",
|
||||
"ohm-js": "^17.1.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-ts-webcompat-resolver": "^1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest --verbose"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
locales
|
||||
@@ -0,0 +1,243 @@
|
||||
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { HDL } from "../languages/hdl.js";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { build, parse } from "./builder.js";
|
||||
import { Chip, HIGH, LOW } from "./chip.js";
|
||||
|
||||
function asDisplay(e: unknown): string {
|
||||
return display(
|
||||
(e as { message: string }).message ??
|
||||
(e as { shortMessage: string }).shortMessage ??
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Chip Builder", () => {
|
||||
it("builds a chip from a string", async () => {
|
||||
const nand = unwrap(
|
||||
await parse(
|
||||
`CHIP Not { IN in; OUT out; PARTS: Nand(a=in, b=in, out=out); }`,
|
||||
),
|
||||
);
|
||||
|
||||
nand.in().pull(LOW);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in().pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(LOW);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subbus components", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(
|
||||
`CHIP Foo {
|
||||
IN six[3];
|
||||
OUT out;
|
||||
PARTS: Not16(
|
||||
in[0..1] = true,
|
||||
in[3..5] = six,
|
||||
in[7] = true,
|
||||
);
|
||||
}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
const six = foo.in("six");
|
||||
six.busVoltage = 6;
|
||||
foo.eval();
|
||||
const inVoltage = [...foo.parts][0].in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b10110011));
|
||||
|
||||
// const outVoltage = foo.pin("out1").busVoltage;
|
||||
// expect(outVoltage).toBe(0b01001);
|
||||
// expect(outVoltage).toBe(0b11001);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subpins", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(`
|
||||
CHIP Not2 {
|
||||
IN in[2];
|
||||
OUT out[2];
|
||||
PARTS:
|
||||
Not(in=in[0], out=out[0]);
|
||||
Not(in=in[1], out=out[1]);
|
||||
}
|
||||
`),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in().busVoltage = 0b00;
|
||||
foo.eval();
|
||||
expect(foo.out().busVoltage).toBe(0b11);
|
||||
|
||||
foo.in().busVoltage = 0b11;
|
||||
foo.eval();
|
||||
expect(foo.out().busVoltage).toBe(0b00);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subbus components on the right", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(
|
||||
`CHIP Foo {
|
||||
IN in[16];
|
||||
OUT out[5];
|
||||
PARTS: Not16(
|
||||
in[0..7] = in[4..11],
|
||||
// in[8..15] = false,
|
||||
out[3..5] = out[1..3],
|
||||
);
|
||||
}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in().busVoltage = 0b1010_1100_0011_0101;
|
||||
foo.eval();
|
||||
const inVoltage = [...foo.parts][0].in().busVoltage;
|
||||
const outVoltage = foo.out().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b11000011));
|
||||
expect(bin(outVoltage)).toBe(bin(0b01110));
|
||||
});
|
||||
|
||||
it("looks up unknown chips in fs", async () => {
|
||||
const fs = new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "Copy.hdl": COPY_HDL }),
|
||||
);
|
||||
|
||||
let foo: Chip;
|
||||
|
||||
try {
|
||||
const chip = unwrap(await HDL.parse(USE_COPY_HDL));
|
||||
foo = unwrap(await build({ parts: chip, dir: ".", fs }));
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in("a").pull(HIGH);
|
||||
foo.eval();
|
||||
expect(foo.out("b").busVoltage).toBe(1);
|
||||
|
||||
foo.in("a").pull(LOW);
|
||||
foo.eval();
|
||||
expect(foo.out("b").busVoltage).toBe(0);
|
||||
});
|
||||
|
||||
it("returns error for mismatching input width", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Foo {
|
||||
IN in[3]; OUT out;
|
||||
PARTS: Or8Way(in=in, out=out);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for mismatching output width", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Foo {
|
||||
IN in; OUT out[5];
|
||||
PARTS: Not(in=in, out=out);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for wire loop", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
Nand(a=in, b=myNand, out=myNand);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for part loop", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
Nand(a=in, b=b, out=c);
|
||||
Nand(a=in, b=c, out=b);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("sorts after wiring", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Or { IN a, b; OUT out;
|
||||
PARTS:
|
||||
Not(in =b , out = net2);
|
||||
Nand(a = net, b =net2 , out =out );
|
||||
Not(in =a , out = net);
|
||||
}`),
|
||||
);
|
||||
const orA = await build({ parts: chip });
|
||||
expect(orA).toBeOk();
|
||||
|
||||
const ora = unwrap(orA);
|
||||
|
||||
ora.in("a").pull(HIGH);
|
||||
ora.in("b").pull(LOW);
|
||||
ora.eval();
|
||||
expect(ora.out("out").busVoltage).toBe(HIGH);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const USE_COPY_HDL = `CHIP UseCopy {
|
||||
IN a; OUT b;
|
||||
PARTS: Copy(in=a, out=b);
|
||||
}`;
|
||||
|
||||
const COPY_HDL = `CHIP Copy {
|
||||
IN in; OUT out;
|
||||
PARTS: Or(a=in, b=in, out=out);
|
||||
}`;
|
||||
@@ -0,0 +1,532 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
isOk,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CompilationError, createError, Span } from "../languages/base.js";
|
||||
import { HDL, HdlParse, Part, PinParts } from "../languages/hdl.js";
|
||||
import { getBuiltinChip, hasBuiltinChip } from "./builtins/index.js";
|
||||
import { Chip, Connection, isConstantPin } from "./chip.js";
|
||||
|
||||
function pinWidth(pin: PinParts): Result<number | undefined, CompilationError> {
|
||||
const start = pin.start ?? 0;
|
||||
if (pin.end === undefined) {
|
||||
return Ok(undefined);
|
||||
}
|
||||
if (pin.end >= start) {
|
||||
return Ok(pin.end - start + 1);
|
||||
}
|
||||
return Err(
|
||||
createError(
|
||||
`Bus start index should be less than or equal to bus end index`,
|
||||
pin.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function parse(
|
||||
code: string,
|
||||
dir?: string,
|
||||
name?: string,
|
||||
fs?: FileSystem,
|
||||
): Promise<Result<Chip, CompilationError>> {
|
||||
const parsed = HDL.parse(code.toString());
|
||||
if (isErr(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return build({ parts: Ok(parsed), dir, name, fs });
|
||||
}
|
||||
|
||||
export async function loadChip(
|
||||
name: string,
|
||||
dir?: string,
|
||||
fs?: FileSystem,
|
||||
): Promise<Result<Chip>> {
|
||||
if (hasBuiltinChip(name) || fs === undefined) {
|
||||
return await getBuiltinChip(name);
|
||||
}
|
||||
try {
|
||||
const file = await fs.readFile(`${dir}/${name}.hdl`);
|
||||
const maybeParsedHDL = HDL.parse(file);
|
||||
|
||||
let maybeChip: Result<Chip, Error>;
|
||||
if (isOk(maybeParsedHDL)) {
|
||||
const maybeBuilt = await build({
|
||||
parts: Ok(maybeParsedHDL),
|
||||
dir,
|
||||
name,
|
||||
fs,
|
||||
});
|
||||
if (isErr(maybeBuilt)) {
|
||||
maybeChip = Err(new Error(Err(maybeBuilt).message));
|
||||
} else {
|
||||
maybeChip = maybeBuilt;
|
||||
}
|
||||
} else {
|
||||
maybeChip = Err(new Error("HDL Was not parsed"));
|
||||
}
|
||||
|
||||
return maybeChip;
|
||||
} catch (_e) {
|
||||
return Err(new Error(`Could not load chip ${name}.hdl` /*, { cause: e }*/));
|
||||
}
|
||||
}
|
||||
|
||||
export async function build(
|
||||
...args: Parameters<typeof ChipBuilder.build>
|
||||
): Promise<ReturnType<typeof ChipBuilder.build>> {
|
||||
return await ChipBuilder.build(...args);
|
||||
}
|
||||
|
||||
interface InternalPin {
|
||||
isDefined: boolean;
|
||||
firstUse: Span;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface WireData {
|
||||
partChip: Chip;
|
||||
lhs: PinParts;
|
||||
rhs: PinParts;
|
||||
}
|
||||
|
||||
function getSubBusWidth(pin: PinParts): number | undefined {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
return pin.end - pin.start + 1;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function display(pin: PinParts): string {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
return `${pin.pin}[${pin.start}..${pin.end}]`;
|
||||
}
|
||||
return pin.pin;
|
||||
}
|
||||
|
||||
function createConnection(
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<Connection, CompilationError> {
|
||||
const lhsWidth = pinWidth(lhs);
|
||||
const rhsWidth = pinWidth(rhs);
|
||||
if (isErr(lhsWidth)) {
|
||||
return lhsWidth;
|
||||
}
|
||||
if (isErr(rhsWidth)) {
|
||||
return rhsWidth;
|
||||
}
|
||||
|
||||
return Ok({
|
||||
to: {
|
||||
name: lhs.pin.toString(),
|
||||
start: lhs.start ?? 0,
|
||||
width: Ok(lhsWidth),
|
||||
},
|
||||
from: {
|
||||
name: rhs.pin.toString(),
|
||||
start: rhs.start ?? 0,
|
||||
width: Ok(rhsWidth),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getIndices(pin: PinParts): number[] {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
const indices = [];
|
||||
for (let i = pin.start; i <= pin.end; i++) {
|
||||
indices.push(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
return [-1];
|
||||
}
|
||||
|
||||
function checkMultipleAssignments(
|
||||
pin: PinParts,
|
||||
assignedIndexes: Map<string, Set<number>>,
|
||||
): Result<void, CompilationError> {
|
||||
let errorIndex: number | undefined = undefined; // -1 stands for the whole bus width
|
||||
const indices = assignedIndexes.get(pin.pin);
|
||||
if (!indices) {
|
||||
assignedIndexes.set(pin.pin, new Set(getIndices(pin)));
|
||||
} else {
|
||||
if (indices.has(-1)) {
|
||||
errorIndex = pin.start ?? -1;
|
||||
} else if (pin.start !== undefined && pin.end !== undefined) {
|
||||
for (const i of getIndices(pin)) {
|
||||
if (indices.has(i)) {
|
||||
errorIndex = i;
|
||||
}
|
||||
indices.add(i);
|
||||
}
|
||||
} else {
|
||||
indices.add(-1);
|
||||
}
|
||||
}
|
||||
if (errorIndex != undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
`Cannot write to pin ${pin.pin}${
|
||||
errorIndex != -1 ? `[${errorIndex}]` : ""
|
||||
} multiple times`,
|
||||
pin.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
class ChipBuilder {
|
||||
private parts: HdlParse;
|
||||
private fs?: FileSystem;
|
||||
private dir?: string;
|
||||
private expectedName?: string;
|
||||
|
||||
private chip: Chip;
|
||||
private internalPins: Map<string, InternalPin> = new Map();
|
||||
private inPins: Map<string, Set<number>> = new Map();
|
||||
private outPins: Map<string, Set<number>> = new Map();
|
||||
private wires: WireData[] = [];
|
||||
|
||||
static build(options: {
|
||||
parts: HdlParse;
|
||||
fs?: FileSystem;
|
||||
dir?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
return new ChipBuilder(options).build();
|
||||
}
|
||||
|
||||
private constructor({
|
||||
parts,
|
||||
fs,
|
||||
dir,
|
||||
name,
|
||||
}: {
|
||||
parts: HdlParse;
|
||||
fs?: FileSystem;
|
||||
dir?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
this.parts = parts;
|
||||
this.expectedName = name;
|
||||
this.dir = dir;
|
||||
this.fs = fs;
|
||||
this.chip = new Chip(
|
||||
parts.ins.map(({ pin, width }) => ({ pin: pin.toString(), width })),
|
||||
parts.outs.map(({ pin, width }) => ({ pin: pin.toString(), width })),
|
||||
parts.name.value,
|
||||
[],
|
||||
parts.clocked,
|
||||
);
|
||||
}
|
||||
|
||||
async build(): Promise<Result<Chip, CompilationError>> {
|
||||
if (this.expectedName && this.parts.name.value != this.expectedName) {
|
||||
return Err(createError(`Wrong chip name`, this.parts.name.span));
|
||||
}
|
||||
|
||||
if (this.parts.parts === "BUILTIN") {
|
||||
return await getBuiltinChip(this.parts.name.value);
|
||||
}
|
||||
|
||||
const result = await this.wireParts();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.chip.clockedPins = new Set(
|
||||
[...this.chip.ins.entries(), ...this.chip.outs.entries()]
|
||||
.map((pin) => pin.name)
|
||||
.filter((pin) => this.chip.isClockedPin(pin)),
|
||||
);
|
||||
|
||||
// Reset clock order after wiring sub-pins
|
||||
for (const part of this.chip.parts) {
|
||||
part.subscribeToClock();
|
||||
}
|
||||
|
||||
return Ok(this.chip);
|
||||
}
|
||||
|
||||
private async wireParts(): Promise<Result<void, CompilationError>> {
|
||||
if (this.parts.parts === "BUILTIN") {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
for (const part of this.parts.parts) {
|
||||
const builtin = await loadChip(part.name, this.dir, this.fs);
|
||||
if (isErr(builtin)) {
|
||||
return Err(createError(`Undefined chip name: ${part.name}`, part.span));
|
||||
}
|
||||
const partChip = Ok(builtin);
|
||||
if (partChip.name == this.chip.name) {
|
||||
return Err(
|
||||
createError(
|
||||
`Cannot use chip ${partChip.name} to implement itself`,
|
||||
part.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
const result = this.wirePart(part, partChip);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
let result = this.validateInternalPins();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
// We need to check this at the end because during wiring we might not know the width of some internal pins
|
||||
result = this.validateWireWidths();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private checkLoops(
|
||||
part: Part,
|
||||
partChip: Chip,
|
||||
): Result<void, CompilationError> {
|
||||
const ins = new Set<string>();
|
||||
const outs = new Set<string>();
|
||||
|
||||
let loop: string | undefined = undefined;
|
||||
for (const { lhs, rhs } of part.wires) {
|
||||
if (partChip.isInPin(lhs.pin)) {
|
||||
if (outs.has(rhs.pin)) {
|
||||
loop = rhs.pin;
|
||||
break;
|
||||
} else {
|
||||
ins.add(rhs.pin);
|
||||
}
|
||||
} else if (partChip.isOutPin(lhs.pin)) {
|
||||
if (ins.has(rhs.pin)) {
|
||||
loop = rhs.pin;
|
||||
break;
|
||||
} else {
|
||||
outs.add(rhs.pin);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loop) {
|
||||
return Err(createError(`Looping wire ${loop}`, part.span));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private wirePart(part: Part, partChip: Chip): Result<void, CompilationError> {
|
||||
const result = this.checkLoops(part, partChip);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const connections: Connection[] = [];
|
||||
this.inPins.clear();
|
||||
for (const { lhs, rhs } of part.wires) {
|
||||
const result = this.validateWire(partChip, lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
const connection = createConnection(lhs, rhs);
|
||||
if (isErr(connection)) {
|
||||
return connection;
|
||||
}
|
||||
connections.push(Ok(connection));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.chip.wire(partChip, connections);
|
||||
if (isErr(result)) {
|
||||
const error = Err(result);
|
||||
return Err(
|
||||
createError(
|
||||
error.message,
|
||||
error.lhs
|
||||
? part.wires[error.wireIndex].lhs.span
|
||||
: part.wires[error.wireIndex].rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.chip.sortParts();
|
||||
return Ok();
|
||||
} catch (e) {
|
||||
return Err(createError((e as Error).message, part.span));
|
||||
}
|
||||
}
|
||||
|
||||
private validateWire(
|
||||
partChip: Chip,
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
if (partChip.isInPin(lhs.pin)) {
|
||||
const result = this.validateInputWire(lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else if (partChip.isOutPin(lhs.pin)) {
|
||||
const result = this.validateOutputWire(partChip, lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return Err(createError(`Undefined pin name: ${lhs.pin}`, lhs.span));
|
||||
}
|
||||
if (!isConstantPin(rhs.pin)) {
|
||||
this.wires.push({ partChip: partChip, lhs, rhs });
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInputWire(
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
let result = this.validateInputSource(rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
result = checkMultipleAssignments(lhs, this.inPins);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// track internal pin use to detect undefined pins
|
||||
if (this.chip.isInternalPin(rhs.pin)) {
|
||||
const pinData = this.internalPins.get(rhs.pin);
|
||||
if (pinData == undefined) {
|
||||
this.internalPins.set(rhs.pin, {
|
||||
isDefined: false,
|
||||
firstUse: rhs.span,
|
||||
});
|
||||
} else {
|
||||
pinData.firstUse =
|
||||
pinData.firstUse.start < rhs.span.start ? pinData.firstUse : rhs.span;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateOutputWire(
|
||||
partChip: Chip,
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
let result = this.validateWriteTarget(rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this.chip.isOutPin(rhs.pin)) {
|
||||
result = checkMultipleAssignments(rhs, this.outPins);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// rhs is necessarily an internal pin
|
||||
if (rhs.start !== undefined || rhs.end !== undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
`Internal pins (in this case: ${rhs.pin}) cannot be subscripted or indexed`,
|
||||
rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
// track internal pin creation to detect undefined pins
|
||||
const pinData = this.internalPins.get(rhs.pin);
|
||||
const width = getSubBusWidth(lhs) ?? partChip.get(lhs.pin)?.width;
|
||||
if (pinData == undefined) {
|
||||
this.internalPins.set(rhs.pin, {
|
||||
isDefined: true,
|
||||
firstUse: rhs.span,
|
||||
width,
|
||||
});
|
||||
} else {
|
||||
if (pinData.isDefined) {
|
||||
return Err(
|
||||
createError(`Internal pin ${rhs.pin} already defined`, rhs.span),
|
||||
);
|
||||
}
|
||||
pinData.isDefined = true;
|
||||
pinData.width = width;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateWriteTarget(rhs: PinParts): Result<void, CompilationError> {
|
||||
if (this.chip.isInPin(rhs.pin)) {
|
||||
return Err(createError(`Cannot write to input pin ${rhs.pin}`, rhs.span));
|
||||
}
|
||||
if (isConstantPin(rhs.pin)) {
|
||||
return Err(
|
||||
createError(`Internal pin name cannot be "true" or "false"`, rhs.span),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInputSource(rhs: PinParts): Result<void, CompilationError> {
|
||||
if (this.chip.isOutPin(rhs.pin)) {
|
||||
return Err(createError(`Cannot use output pin as input`, rhs.span));
|
||||
} else if (!this.chip.isInPin(rhs.pin) && rhs.start != undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
isConstantPin(rhs.pin)
|
||||
? `Constant bus cannot be subscripted or indexed`
|
||||
: `Internal pins (in this case: ${rhs.pin}) cannot be subscripted or indexed`,
|
||||
rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInternalPins(): Result<void, CompilationError> {
|
||||
for (const [name, pinData] of this.internalPins) {
|
||||
if (!pinData.isDefined) {
|
||||
return Err(
|
||||
createError(
|
||||
name.toLowerCase() == "true" || name.toLowerCase() == "false"
|
||||
? `The constant bus ${name.toLowerCase()} must be in lower-case`
|
||||
: `Undefined internal pin name: ${name}`,
|
||||
pinData.firstUse,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateWireWidths(): Result<void, CompilationError> {
|
||||
for (const wire of this.wires) {
|
||||
const lhsWidth =
|
||||
getSubBusWidth(wire.lhs) ?? wire.partChip.get(wire.lhs.pin)?.width;
|
||||
const rhsWidth =
|
||||
getSubBusWidth(wire.rhs) ??
|
||||
this.chip.get(wire.rhs.pin)?.width ??
|
||||
this.internalPins.get(wire.rhs.pin)?.width;
|
||||
if (lhsWidth != rhsWidth) {
|
||||
return Err(
|
||||
createError(
|
||||
`Different bus widths: ${display(
|
||||
wire.lhs,
|
||||
)}(${lhsWidth}) and ${display(wire.rhs)}(${rhsWidth})`,
|
||||
{
|
||||
start: wire.lhs.span.start,
|
||||
end: wire.rhs.span.end,
|
||||
line: wire.lhs.span.line,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Memory } from "./builtins/computer/computer.js";
|
||||
import { RAM } from "./builtins/sequential/ram.js";
|
||||
import { Chip, Pin } from "./chip.js";
|
||||
|
||||
export function getBuiltinValue(
|
||||
chip: string,
|
||||
part: Chip,
|
||||
idx: number,
|
||||
): Pin | undefined {
|
||||
switch (chip) {
|
||||
case "Register":
|
||||
case "ARegister":
|
||||
case "DRegister":
|
||||
case "PC":
|
||||
case "KEYBOARD":
|
||||
return part.out();
|
||||
case "RAM8":
|
||||
case "RAM64":
|
||||
case "RAM512":
|
||||
case "RAM4K":
|
||||
case "RAM16K":
|
||||
case "ROM32K":
|
||||
case "Screen":
|
||||
return (part as RAM).at(idx);
|
||||
case "Memory":
|
||||
return (part as Memory).at(idx);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Ok, unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CHIP_PROJECTS } from "@nand2tetris/projects/base.js";
|
||||
import { ChipProjects } from "@nand2tetris/projects/full.js";
|
||||
import { Max } from "@nand2tetris/projects/samples/hack.js";
|
||||
import { compare } from "../../compare.js";
|
||||
import { CMP, Cmp } from "../../languages/cmp.js";
|
||||
import { HDL, HdlParse } from "../../languages/hdl.js";
|
||||
import { TST, Tst } from "../../languages/tst.js";
|
||||
import { ChipTest } from "../../test/chiptst.js";
|
||||
import { build } from "../builder.js";
|
||||
import { Chip } from "../chip.js";
|
||||
|
||||
const SKIP = new Set<string>(["Computer", "Memory"]);
|
||||
|
||||
describe("All Projects", () => {
|
||||
describe.each(Object.keys(CHIP_PROJECTS))("project %s", (project) => {
|
||||
it.each(
|
||||
CHIP_PROJECTS[project as keyof typeof CHIP_PROJECTS].filter(
|
||||
(k) => !SKIP.has(k),
|
||||
),
|
||||
)("Builtin %s", async (chipName) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const ChipProject = ChipProjects[project].CHIPS;
|
||||
let hdlFile: string = ChipProject[`${chipName}.hdl`];
|
||||
const tstFile: string = ChipProject[`${chipName}.tst`];
|
||||
const cmpFile: string = ChipProject[`${chipName}.cmp`];
|
||||
|
||||
expect(hdlFile).toBeDefined();
|
||||
expect(tstFile).toBeDefined();
|
||||
expect(cmpFile).toBeDefined();
|
||||
|
||||
const partsIdx = hdlFile.indexOf("PARTS:");
|
||||
expect(partsIdx).toBeGreaterThan(0);
|
||||
hdlFile = hdlFile.substring(0, partsIdx) + "BUILTIN; }";
|
||||
const hdl = HDL.parse(hdlFile);
|
||||
expect(hdl).toBeOk();
|
||||
const tst = TST.parse(tstFile);
|
||||
expect(tst).toBeOk();
|
||||
|
||||
const chip = await build({ parts: Ok(hdl as Ok<HdlParse>) });
|
||||
expect(chip).toBeOk();
|
||||
const test = unwrap(ChipTest.from(Ok(tst as Ok<Tst>))).with(
|
||||
Ok(chip as Ok<Chip>),
|
||||
);
|
||||
|
||||
if (project === "05") {
|
||||
test.setFileSystem(
|
||||
new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "/samples/Max.hack": Max }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await test.run();
|
||||
|
||||
const outFile = test.log();
|
||||
|
||||
const cmp = CMP.parse(cmpFile);
|
||||
expect(cmp).toBeOk();
|
||||
const out = CMP.parse(outFile);
|
||||
expect(out).toBeOk();
|
||||
|
||||
const diffs = compare(Ok(cmp as Ok<Cmp>), Ok(out as Ok<Cmp>));
|
||||
expect(diffs).toHaveNoDiff();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Chip } from "../../chip.js";
|
||||
|
||||
export function add16(a: number, b: number): [number] {
|
||||
return [(a + b) & 0xffff];
|
||||
}
|
||||
|
||||
export class Add16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"], "Add16");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = add16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { alu, alua, COMMANDS_OP, Flags } from "../../../cpu/alu.js";
|
||||
import { Chip, HIGH, LOW } from "../../chip.js";
|
||||
|
||||
export class ALUNoStat extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out] = alu(op, x, y);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class ALU extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
"zr", // 1 if (out === 0), 0 otherwise
|
||||
"ng", // 1 if (out < 0), 0 otherwise
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out, flags] = alu(op, x, y);
|
||||
|
||||
const ng = flags === Flags.Negative ? HIGH : LOW;
|
||||
const zr = flags === Flags.Zero ? HIGH : LOW;
|
||||
this.out("out").busVoltage = out;
|
||||
this.out("ng").pull(ng);
|
||||
this.out("zr").pull(zr);
|
||||
}
|
||||
|
||||
op(): COMMANDS_OP {
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
return op as COMMANDS_OP;
|
||||
}
|
||||
}
|
||||
|
||||
export class ALUAll extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
"zr", // 1 if (out === 0), 0 otherwise
|
||||
"ng", // 1 if (out < 0), 0 otherwise
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out, flags] = alua(op, x, y);
|
||||
|
||||
const ng = flags === Flags.Negative ? HIGH : LOW;
|
||||
const zr = flags === Flags.Zero ? HIGH : LOW;
|
||||
this.out("out").busVoltage = out;
|
||||
this.out("ng").pull(ng);
|
||||
this.out("zr").pull(zr);
|
||||
}
|
||||
|
||||
op(): COMMANDS_OP {
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
return op as COMMANDS_OP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Chip, Voltage } from "../../chip.js";
|
||||
import { or } from "../logic/or.js";
|
||||
import { halfAdder } from "./half_adder.js";
|
||||
|
||||
export function fullAdder(
|
||||
a: Voltage,
|
||||
b: Voltage,
|
||||
c: Voltage,
|
||||
): [Voltage, Voltage] {
|
||||
const [s, ca] = halfAdder(a, b);
|
||||
const [sum, cb] = halfAdder(s, c);
|
||||
const [carry] = or(ca, cb);
|
||||
|
||||
return [sum, carry];
|
||||
}
|
||||
|
||||
export class FullAdder extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b", "c"], ["sum", "carry"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const c = this.in("c").voltage();
|
||||
const [sum, carry] = fullAdder(a, b, c);
|
||||
this.out("sum").pull(sum);
|
||||
this.out("carry").pull(carry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function halfAdder(a: Voltage, b: Voltage): [Voltage, Voltage] {
|
||||
const sum = (a === 1 && b === 0) || (a === 0 && b === 1) ? HIGH : LOW;
|
||||
const car = a === 1 && b === 1 ? HIGH : LOW;
|
||||
|
||||
return [sum, car];
|
||||
}
|
||||
|
||||
export class HalfAdder extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["sum", "carry"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [sum, carry] = halfAdder(a, b);
|
||||
this.out("sum").pull(sum);
|
||||
this.out("carry").pull(carry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Chip } from "../../chip.js";
|
||||
import { add16 } from "./add_16.js";
|
||||
|
||||
export function inc16(n: number): [number] {
|
||||
return add16(n, 1);
|
||||
}
|
||||
|
||||
export class Inc16 extends Chip {
|
||||
constructor() {
|
||||
super(["in[16]"], ["out[16]"], "Inc16");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in().busVoltage;
|
||||
const [out] = inc16(a);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const builtinOverrides: Record<string, string> = {
|
||||
CPU: `CHIP CPU {
|
||||
|
||||
IN inM[16], // M value input (M = contents of RAM[A])
|
||||
instruction[16], // Instruction for execution
|
||||
reset; // Signals whether to re-start the current
|
||||
// program (reset==1) or continue executing
|
||||
// the current program (reset==0).
|
||||
|
||||
OUT outM[16], // M value output
|
||||
writeM, // Write to M?
|
||||
addressM[15], // Address in data memory (of M)
|
||||
pc[15]; // address of next instruction
|
||||
|
||||
PARTS:
|
||||
Mux16(a=instruction, b=ALUoutput, sel=instruction[15], out=Ainput);
|
||||
Not(in=instruction[15], out=Ainstruction);
|
||||
Or(a=Ainstruction, b=instruction[5], out=loadA);
|
||||
ARegister(in=Ainput, load=loadA, out=Aoutput, out[0..14]=addressM);
|
||||
|
||||
And(a=instruction[15], b=instruction[4], out=loadD);
|
||||
DRegister(in=ALUoutput, load=loadD, out=Doutput);
|
||||
|
||||
Mux16(a=Aoutput, b=inM, sel=instruction[12], out=ALUsecondInput);
|
||||
|
||||
ALU(x=Doutput, y=ALUsecondInput,
|
||||
zx=instruction[11], nx=instruction[10],
|
||||
zy=instruction[9], ny=instruction[8],
|
||||
f=instruction[7], no=instruction[6],
|
||||
out=ALUoutput, out=outM, ng=negative, zr=zero);
|
||||
|
||||
And(a=instruction[15], b=instruction[3], out=writeM);
|
||||
|
||||
Or(a=negative, b=zero, out=notPositive);
|
||||
Not(in=notPositive, out=positive);
|
||||
|
||||
And(a=positive, b=instruction[0], out=j1);
|
||||
And(a=zero, b=instruction[1], out=j2);
|
||||
And(a=negative, b=instruction[2], out=j3);
|
||||
Or(a=j1, b=j2, out=jTemp);
|
||||
Or(a=jTemp, b=j3, out=jumpIfC);
|
||||
And(a=jumpIfC, b=instruction[15], out=jump);
|
||||
|
||||
PC(reset=reset, inc=true, load=jump, in=Aoutput, out[0..14]=pc);
|
||||
}`,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# Computer Builtins
|
||||
|
||||
## CPU
|
||||
|
||||
<iframe style="border: 1px solid rgba(0, 0, 0, 0.1);" width="800" height="450" src="https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FNR57Ym7f7zUowcs0jrtTsO%2FHack-CPU%3Fnode-id%3D0%253A1" allowfullscreen></iframe>
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Max } from "@nand2tetris/projects/samples/hack.js";
|
||||
import { HIGH } from "../../chip.js";
|
||||
import { CPU, Memory, ROM32K } from "./computer.js";
|
||||
|
||||
describe("Computer Chip Builtins", () => {
|
||||
describe("ROM Builtin", () => {
|
||||
it("can load a file", async () => {
|
||||
const fs = new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "Max.hack": Max }),
|
||||
);
|
||||
const rom = new ROM32K();
|
||||
|
||||
await rom.load(fs, "Max.hack");
|
||||
|
||||
expect(rom.at(4).busVoltage).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CPU Chip Builtin", () => {
|
||||
it("updates PC on tock", () => {
|
||||
const cpu = new CPU();
|
||||
|
||||
cpu.in("instruction").busVoltage = 12345;
|
||||
|
||||
cpu.tick();
|
||||
|
||||
expect(cpu.out("pc").busVoltage).toBe(0);
|
||||
|
||||
cpu.tock();
|
||||
|
||||
expect(cpu.out("pc").busVoltage).toBe(1);
|
||||
});
|
||||
|
||||
it("updtates writeM on tick", () => {
|
||||
const cpu = new CPU();
|
||||
|
||||
cpu.in("instruction").busVoltage = 0b1110_1111_1100_1000;
|
||||
|
||||
cpu.tick();
|
||||
|
||||
expect(cpu.out("writeM").voltage()).toBe(HIGH);
|
||||
expect(cpu.out("outM").busVoltage).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory", () => {
|
||||
it("maps addresses greater than KBD as 0", () => {
|
||||
const memory = new Memory();
|
||||
memory.in("address").busVoltage = 24577;
|
||||
memory.in("in").busVoltage = 47;
|
||||
|
||||
memory.eval();
|
||||
|
||||
expect(memory.out().busVoltage).toBe(0);
|
||||
|
||||
memory.in("load").busVoltage = HIGH;
|
||||
|
||||
memory.eval();
|
||||
|
||||
expect(memory.out().busVoltage).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
CPUInput,
|
||||
CPUState,
|
||||
cpuTick,
|
||||
cpuTock,
|
||||
emptyState,
|
||||
} from "../../../cpu/cpu.js";
|
||||
import {
|
||||
KEYBOARD_OFFSET,
|
||||
KeyboardAdapter,
|
||||
SCREEN_OFFSET,
|
||||
SCREEN_SIZE,
|
||||
} from "../../../cpu/memory.js";
|
||||
import { load } from "../../../fs.js";
|
||||
import { int10 } from "../../../util/twos.js";
|
||||
import {
|
||||
Bus,
|
||||
Chip,
|
||||
ClockedChip,
|
||||
ConstantBus,
|
||||
FALSE_BUS,
|
||||
HIGH,
|
||||
LOW,
|
||||
Pin,
|
||||
} from "../../chip.js";
|
||||
import { RAM, RAM16K } from "../sequential/ram.js";
|
||||
|
||||
export class ROM32K extends RAM {
|
||||
constructor() {
|
||||
super(15, "ROM");
|
||||
}
|
||||
|
||||
override async load(fs: FileSystem, path: string) {
|
||||
try {
|
||||
(await load(fs, path)).map((v, i) => (this.at(i).busVoltage = v));
|
||||
} catch (cause) {
|
||||
// throw new Error(`ROM32K Failed to load file ${path}`, { cause });
|
||||
throw new Error(`ROM32K Failed to load file ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Screen extends RAM {
|
||||
static readonly SIZE = SCREEN_SIZE;
|
||||
static readonly OFFSET = SCREEN_OFFSET;
|
||||
|
||||
constructor() {
|
||||
super(13, "Screen");
|
||||
}
|
||||
}
|
||||
|
||||
export class Keyboard extends Chip implements KeyboardAdapter {
|
||||
static readonly OFFSET = KEYBOARD_OFFSET;
|
||||
|
||||
constructor() {
|
||||
super([], ["out[16]"], "Keyboard");
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.out().busVoltage;
|
||||
}
|
||||
|
||||
setKey(key: number) {
|
||||
this.out().busVoltage = key & 0xffff;
|
||||
}
|
||||
|
||||
clearKey() {
|
||||
this.out().busVoltage = 0;
|
||||
}
|
||||
|
||||
override get(name: string) {
|
||||
return name === this.name
|
||||
? new ConstantBus(this.name, this.getKey()) // readonly
|
||||
: super.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class Memory extends ClockedChip {
|
||||
readonly ram = new RAM16K();
|
||||
readonly screen = new Screen();
|
||||
private keyboard = new Keyboard();
|
||||
private address = 0;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
["in[16]", "load", "address[15])"],
|
||||
["out[16]"],
|
||||
"Memory",
|
||||
[],
|
||||
["in", "load"]
|
||||
);
|
||||
this.parts.push(this.keyboard);
|
||||
this.parts.push(this.screen);
|
||||
this.parts.push(this.ram);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
const load = this.in("load").voltage();
|
||||
this.address = this.in("address").busVoltage;
|
||||
if (load) {
|
||||
const inn = this.in().busVoltage;
|
||||
if (this.address > Keyboard.OFFSET) {
|
||||
// out of "physical" bounds, should result in some kind of issue...
|
||||
}
|
||||
if (this.address == Keyboard.OFFSET) {
|
||||
// Keyboard, do nothing
|
||||
} else if (this.address >= Screen.OFFSET) {
|
||||
this.screen.at(this.address - Screen.OFFSET).busVoltage = inn;
|
||||
} else {
|
||||
this.ram.at(this.address).busVoltage = inn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
override eval() {
|
||||
if (!this.ram) return;
|
||||
this.address = this.in("address").busVoltage;
|
||||
let out = 0;
|
||||
if (this.address > Keyboard.OFFSET) {
|
||||
// out of "physical" bounds, should result in some kind of issue...
|
||||
} else if (this.address == Keyboard.OFFSET) {
|
||||
out = this.keyboard?.out().busVoltage ?? 0;
|
||||
} else if (this.address >= Screen.OFFSET) {
|
||||
out = this.screen?.at(this.address - Screen.OFFSET).busVoltage ?? 0;
|
||||
} else {
|
||||
out = this.ram?.at(this.address).busVoltage ?? 0;
|
||||
}
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
|
||||
override in(pin?: string): Pin {
|
||||
if (pin?.startsWith("RAM16K")) {
|
||||
const idx = int10(pin.match(/\[(?<idx>\d+)]/)?.groups?.idx ?? "0");
|
||||
return this.ram.at(idx);
|
||||
}
|
||||
if (pin?.startsWith("Screen")) {
|
||||
const idx = int10(pin.match(/\[(?<idx>\d+)]/)?.groups?.idx ?? "0");
|
||||
return this.screen.at(idx);
|
||||
}
|
||||
if (pin?.startsWith("Keyboard")) {
|
||||
return this.keyboard.out();
|
||||
}
|
||||
return super.in(pin);
|
||||
}
|
||||
|
||||
override get(name: string, offset = 0): Pin | undefined {
|
||||
if (name.startsWith("RAM16K")) {
|
||||
return this.at(offset & 0x3fff);
|
||||
}
|
||||
if (name.startsWith("Screen")) {
|
||||
return this.at(offset & (0x1fff + Screen.OFFSET));
|
||||
}
|
||||
if (name.startsWith("Keyboard")) {
|
||||
return this.at(Keyboard.OFFSET);
|
||||
}
|
||||
if (name.startsWith("Memory")) {
|
||||
return this.at(offset);
|
||||
}
|
||||
return super.get(name, offset);
|
||||
}
|
||||
|
||||
at(offset: number): Pin {
|
||||
if (offset > Keyboard.OFFSET) {
|
||||
return FALSE_BUS;
|
||||
}
|
||||
if (offset == Keyboard.OFFSET) {
|
||||
return this.keyboard.out();
|
||||
}
|
||||
if (offset >= Screen.OFFSET) {
|
||||
return this.screen.at(offset - Screen.OFFSET);
|
||||
}
|
||||
return this.ram.at(offset);
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this.address = 0;
|
||||
this.ram.reset();
|
||||
this.screen.reset();
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
class DRegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.D;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.D = num;
|
||||
}
|
||||
}
|
||||
|
||||
class ARegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.A;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.A = num;
|
||||
}
|
||||
}
|
||||
|
||||
class PCBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.PC;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.PC = num;
|
||||
}
|
||||
}
|
||||
|
||||
export class CPU extends ClockedChip {
|
||||
private _state: CPUState = emptyState();
|
||||
|
||||
get state(): CPUState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
["inM[16]", "instruction[16]", "reset"],
|
||||
["outM[16]", "writeM", "addressM[15]", "pc[15]"],
|
||||
"CPU",
|
||||
[],
|
||||
["pc", "addressM", "reset"],
|
||||
);
|
||||
}
|
||||
|
||||
override tick(): void {
|
||||
const [state, writeM] = cpuTick(this.cpuInput(), this._state);
|
||||
this._state = state;
|
||||
this.out("writeM").pull(writeM ? HIGH : LOW);
|
||||
this.out("outM").busVoltage = this._state.ALU ?? 0;
|
||||
}
|
||||
|
||||
override tock(): void {
|
||||
if (!this._state) return; // Skip initial tock
|
||||
const [output, state] = cpuTock(this.cpuInput(), this._state);
|
||||
this._state = state;
|
||||
|
||||
this.out("addressM").busVoltage = output.addressM ?? 0;
|
||||
this.out("outM").busVoltage = output.outM ?? 0;
|
||||
this.out("writeM").pull(output.writeM ? HIGH : LOW);
|
||||
this.out("pc").busVoltage = this._state?.PC ?? 0;
|
||||
}
|
||||
|
||||
private cpuInput(): CPUInput {
|
||||
const inM = this.in("inM").busVoltage;
|
||||
const instruction = this.in("instruction").busVoltage;
|
||||
const reset = this.in("reset").busVoltage === 1;
|
||||
return { inM, instruction, reset };
|
||||
}
|
||||
|
||||
override get(pin: string, offset?: number): Pin | undefined {
|
||||
if (pin?.startsWith("ARegister")) {
|
||||
return new ARegisterBus("ARegister", this._state);
|
||||
}
|
||||
if (pin?.startsWith("DRegister")) {
|
||||
return new DRegisterBus("DRegister", this._state);
|
||||
}
|
||||
if (pin?.startsWith("PC")) {
|
||||
return new PCBus("PC", this._state);
|
||||
}
|
||||
return super.get(pin, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this._state = emptyState();
|
||||
|
||||
// This is a bit of a hack, but because super.reset() does ticktock,
|
||||
// we need to set PC to -1, so that it will be 0 after the reset
|
||||
this._state.PC = -1;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class Computer extends Chip {
|
||||
readonly cpu = new CPU();
|
||||
readonly ram = new Memory();
|
||||
readonly rom = new ROM32K();
|
||||
|
||||
constructor() {
|
||||
super(["reset"], []);
|
||||
|
||||
this.wire(this.cpu, [
|
||||
{ from: { name: "reset", start: 0 }, to: { name: "reset", start: 0 } },
|
||||
{
|
||||
from: { name: "instruction", start: 0 },
|
||||
to: { name: "instruction", start: 0 },
|
||||
},
|
||||
{ from: { name: "oldOutM", start: 0 }, to: { name: "inM", start: 0 } },
|
||||
{ from: { name: "writeM", start: 0 }, to: { name: "writeM", start: 0 } },
|
||||
{
|
||||
from: { name: "addressM", start: 0 },
|
||||
to: { name: "addressM", start: 0 },
|
||||
},
|
||||
{ from: { name: "newInM", start: 0 }, to: { name: "outM", start: 0 } },
|
||||
{ from: { name: "pc", start: 0 }, to: { name: "pc", start: 0 } },
|
||||
]);
|
||||
|
||||
this.wire(this.rom, [
|
||||
{ from: { name: "pc", start: 0 }, to: { name: "address", start: 0 } },
|
||||
{
|
||||
from: { name: "instruction", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
this.wire(this.ram, [
|
||||
{ from: { name: "newInM", start: 0 }, to: { name: "in", start: 0 } },
|
||||
{ from: { name: "writeM", start: 0 }, to: { name: "load", start: 0 } },
|
||||
{
|
||||
from: { name: "addressM", start: 0 },
|
||||
to: { name: "address", start: 0 },
|
||||
},
|
||||
{ from: { name: "oldOutM", start: 0 }, to: { name: "out", start: 0 } },
|
||||
]);
|
||||
|
||||
for (const pin of [...this.ins.entries(), ...this.outs.entries()]) {
|
||||
if (this.isClockedPin(pin.name)) {
|
||||
this.clockedPins.add(pin.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override eval() {
|
||||
super.eval();
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
if (
|
||||
name.startsWith("PC") ||
|
||||
name.startsWith("ARegister") ||
|
||||
name.startsWith("DRegister")
|
||||
) {
|
||||
return this.cpu.get(name);
|
||||
}
|
||||
if (name.startsWith("RAM16K")) {
|
||||
return this.ram.get(name, offset);
|
||||
}
|
||||
return super.get(name, offset);
|
||||
}
|
||||
|
||||
override async load(fs: FileSystem, path: string): Promise<void> {
|
||||
return await this.rom.load(fs, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { parse } from "../builder.js";
|
||||
import { Chip } from "../chip.js";
|
||||
import { Add16 } from "./arithmetic/add_16.js";
|
||||
import { ALU, ALUNoStat } from "./arithmetic/alu.js";
|
||||
import { FullAdder } from "./arithmetic/full_adder.js";
|
||||
import { HalfAdder } from "./arithmetic/half_adder.js";
|
||||
import { Inc16 } from "./arithmetic/inc16.js";
|
||||
import { builtinOverrides } from "./builtinOverrides.js";
|
||||
import {
|
||||
Computer,
|
||||
CPU,
|
||||
Keyboard,
|
||||
Memory,
|
||||
ROM32K,
|
||||
Screen,
|
||||
} from "./computer/computer.js";
|
||||
import { And, And16 } from "./logic/and.js";
|
||||
import { DMux, DMux4Way, DMux8Way } from "./logic/dmux.js";
|
||||
import { Mux, Mux4Way16, Mux8Way16, Mux16 } from "./logic/mux.js";
|
||||
import { Nand, Nand16 } from "./logic/nand.js";
|
||||
import { Not, Not16 } from "./logic/not.js";
|
||||
import { Or, Or8way, Or16 } from "./logic/or.js";
|
||||
import { Xor, Xor16 } from "./logic/xor.js";
|
||||
import { Bit, PC, Register, VRegister } from "./sequential/bit.js";
|
||||
import { DFF } from "./sequential/dff.js";
|
||||
import { RAM4K, RAM8, RAM16K, RAM64, RAM512 } from "./sequential/ram.js";
|
||||
|
||||
export {
|
||||
Add16,
|
||||
ALU,
|
||||
And,
|
||||
And16,
|
||||
VRegister as ARegister,
|
||||
Bit,
|
||||
DFF,
|
||||
DMux,
|
||||
VRegister as DRegister,
|
||||
FullAdder,
|
||||
HalfAdder,
|
||||
Inc16,
|
||||
Mux,
|
||||
Mux16,
|
||||
Mux4Way16,
|
||||
Mux8Way16,
|
||||
Nand,
|
||||
Nand16,
|
||||
Not,
|
||||
Not16,
|
||||
Or,
|
||||
Or16,
|
||||
Or8way,
|
||||
RAM16K,
|
||||
RAM4K,
|
||||
RAM512,
|
||||
RAM64,
|
||||
RAM8,
|
||||
Register,
|
||||
Xor,
|
||||
Xor16,
|
||||
};
|
||||
|
||||
export const REGISTRY = new Map<string, () => Chip>(
|
||||
(
|
||||
[
|
||||
["Nand", Nand],
|
||||
["Nand16", Nand16],
|
||||
["Not", Not],
|
||||
["Not16", Not16],
|
||||
["And", And],
|
||||
["And16", And16],
|
||||
["Or", Or],
|
||||
["Or16", Or16],
|
||||
["Or8Way", Or8way],
|
||||
["XOr", Xor],
|
||||
["XOr16", Xor16],
|
||||
["Xor", Xor],
|
||||
["Xor16", Xor16],
|
||||
["Mux", Mux],
|
||||
["Mux16", Mux16],
|
||||
["Mux4Way16", Mux4Way16],
|
||||
["Mux8Way16", Mux8Way16],
|
||||
["DMux", DMux],
|
||||
["DMux4Way", DMux4Way],
|
||||
["DMux8Way", DMux8Way],
|
||||
["HalfAdder", HalfAdder],
|
||||
["FullAdder", FullAdder],
|
||||
["Add16", Add16],
|
||||
["Inc16", Inc16],
|
||||
["ALU", ALU],
|
||||
["ALUNoStat", ALUNoStat],
|
||||
["DFF", DFF],
|
||||
["Bit", Bit],
|
||||
["Register", Register],
|
||||
["ARegister", Register],
|
||||
["DRegister", Register],
|
||||
["PC", PC],
|
||||
["RAM8", RAM8],
|
||||
["RAM64", RAM64],
|
||||
["RAM512", RAM512],
|
||||
["RAM4K", RAM4K],
|
||||
["RAM16K", RAM16K],
|
||||
["ROM32K", ROM32K],
|
||||
["Screen", Screen],
|
||||
["Keyboard", Keyboard],
|
||||
["CPU", CPU],
|
||||
["Computer", Computer],
|
||||
["Memory", Memory],
|
||||
["ARegister", VRegister],
|
||||
["DRegister", VRegister],
|
||||
] as [string, { new (): Chip }][]
|
||||
).map(([name, ChipCtor]) => [
|
||||
name,
|
||||
() => {
|
||||
const chip = new ChipCtor();
|
||||
chip.name = name;
|
||||
return chip;
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export function hasBuiltinChip(name: string): boolean {
|
||||
return REGISTRY.has(name);
|
||||
}
|
||||
|
||||
export async function getBuiltinChip(name: string): Promise<Result<Chip>> {
|
||||
if (builtinOverrides[name]) {
|
||||
const result = await parse(builtinOverrides[name], name);
|
||||
if (isErr(result)) {
|
||||
return Err(new Error(Err(result).message));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const chip = REGISTRY.get(name);
|
||||
return chip
|
||||
? Ok(chip())
|
||||
: Err(new Error(`Chip ${name} not in builtin registry`));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function and(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 && b === 1 ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function and16(a: number, b: number): [number] {
|
||||
return [a & b & 0xffff];
|
||||
}
|
||||
|
||||
export class And extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [n] = and(a, b);
|
||||
this.out().pull(n);
|
||||
}
|
||||
}
|
||||
|
||||
export class And16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [n] = and16(a, b);
|
||||
this.out().busVoltage = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function dmux(inn: Voltage, sel: Voltage): [Voltage, Voltage] {
|
||||
const a = sel === LOW && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === HIGH && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b];
|
||||
}
|
||||
|
||||
export function dmux4way(
|
||||
inn: Voltage,
|
||||
sel: number,
|
||||
): [Voltage, Voltage, Voltage, Voltage] {
|
||||
const a = sel === 0b00 && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === 0b01 && inn === HIGH ? HIGH : LOW;
|
||||
const c = sel === 0b10 && inn === HIGH ? HIGH : LOW;
|
||||
const d = sel === 0b11 && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b, c, d];
|
||||
}
|
||||
|
||||
export function dmux8way(
|
||||
inn: Voltage,
|
||||
sel: number,
|
||||
): [Voltage, Voltage, Voltage, Voltage, Voltage, Voltage, Voltage, Voltage] {
|
||||
const a = sel === 0b000 && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === 0b001 && inn === HIGH ? HIGH : LOW;
|
||||
const c = sel === 0b010 && inn === HIGH ? HIGH : LOW;
|
||||
const d = sel === 0b011 && inn === HIGH ? HIGH : LOW;
|
||||
const e = sel === 0b100 && inn === HIGH ? HIGH : LOW;
|
||||
const f = sel === 0b101 && inn === HIGH ? HIGH : LOW;
|
||||
const g = sel === 0b110 && inn === HIGH ? HIGH : LOW;
|
||||
const h = sel === 0b111 && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b, c, d, e, f, g, h];
|
||||
}
|
||||
|
||||
export class DMux extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel"], ["a", "b"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").voltage();
|
||||
|
||||
const [a, b] = dmux(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
}
|
||||
}
|
||||
|
||||
export class DMux4Way extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel[2]"], ["a", "b", "c", "d"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").busVoltage;
|
||||
|
||||
const [a, b, c, d] = dmux4way(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
this.out("c").pull(c);
|
||||
this.out("d").pull(d);
|
||||
}
|
||||
}
|
||||
|
||||
export class DMux8Way extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel[3]"], ["a", "b", "c", "d", "e", "f", "g", "h"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").busVoltage;
|
||||
|
||||
const [a, b, c, d, e, f, g, h] = dmux8way(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
this.out("c").pull(c);
|
||||
this.out("d").pull(d);
|
||||
this.out("e").pull(e);
|
||||
this.out("f").pull(f);
|
||||
this.out("g").pull(g);
|
||||
this.out("h").pull(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Chip, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function mux(a: Voltage, b: Voltage, sel: Voltage): [Voltage] {
|
||||
return [sel === LOW ? a : b];
|
||||
}
|
||||
|
||||
export function mux16(a: number, b: number, sel: Voltage): [number] {
|
||||
return [sel === LOW ? a : b];
|
||||
}
|
||||
|
||||
export function mux16_4(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
sel: number,
|
||||
): [number] {
|
||||
const s2 = (sel & 0b01) as Voltage;
|
||||
return (sel & 0b10) === 0b00 ? mux16(a, b, s2) : mux16(c, d, s2);
|
||||
}
|
||||
|
||||
export function mux16_8(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number,
|
||||
g: number,
|
||||
h: number,
|
||||
sel: number,
|
||||
): [number] {
|
||||
const s2 = (sel & 0b11) as Voltage;
|
||||
return (sel & 0b100) === 0b000
|
||||
? mux16_4(a, b, c, d, s2)
|
||||
: mux16_4(e, f, g, h, s2);
|
||||
}
|
||||
|
||||
export class Mux extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b", "sel"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const sel = this.in("sel").voltage();
|
||||
|
||||
const [set] = mux(a, b, sel);
|
||||
this.out().pull(set);
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]", "sel"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const sel = this.in("sel").voltage();
|
||||
const [out] = mux16(a, b, sel);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux4Way16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]", "c[16]", "d[16]", "sel[2]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const c = this.in("c").busVoltage;
|
||||
const d = this.in("d").busVoltage;
|
||||
const sel = this.in("sel").busVoltage;
|
||||
const [out] = mux16_4(a, b, c, d, sel);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux8Way16 extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"a[16]",
|
||||
"b[16]",
|
||||
"c[16]",
|
||||
"d[16]",
|
||||
"e[16]",
|
||||
"f[16]",
|
||||
"g[16]",
|
||||
"h[16]",
|
||||
"sel[3]",
|
||||
],
|
||||
["out[16]"],
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const c = this.in("c").busVoltage;
|
||||
const d = this.in("d").busVoltage;
|
||||
const e = this.in("e").busVoltage;
|
||||
const f = this.in("f").busVoltage;
|
||||
const g = this.in("g").busVoltage;
|
||||
const h = this.in("h").busVoltage;
|
||||
const sel = this.in("sel").busVoltage;
|
||||
const [out] = mux16_8(a, b, c, d, e, f, g, h, sel);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { nand16 } from "../../../util/twos.js";
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function nand(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 && b === 1 ? LOW : HIGH];
|
||||
}
|
||||
|
||||
export class Nand extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = nand(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Nand16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
this.out().busVoltage = nand16(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function not(inn: Voltage): [Voltage] {
|
||||
return [inn === LOW ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function not16(inn: number): [number] {
|
||||
return [~inn & 0xffff];
|
||||
}
|
||||
|
||||
export class Not extends Chip {
|
||||
constructor() {
|
||||
super(["in"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("in").voltage();
|
||||
const [out] = not(a);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Not16 extends Chip {
|
||||
constructor() {
|
||||
super(["in[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const [n] = not16(this.in().busVoltage);
|
||||
this.out().busVoltage = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function or(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 || b === 1 ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function or16(a: number, b: number): [number] {
|
||||
return [(a | b) & 0xffff];
|
||||
}
|
||||
|
||||
export function or8way(a: number): [Voltage] {
|
||||
return [(a & 0xff) === 0 ? LOW : HIGH];
|
||||
}
|
||||
|
||||
export class Or extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = or(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Or16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = or16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Or8way extends Chip {
|
||||
constructor() {
|
||||
super(["in[8]"], ["out"], "Or8way");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const [out] = or8way(inn);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function xor(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [(a === HIGH && b === LOW) || (a === LOW && b === HIGH) ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function xor16(a: number, b: number): [number] {
|
||||
return [(a ^ b) & 0xffff];
|
||||
}
|
||||
|
||||
export class Xor extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = xor(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Xor16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = xor16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Bus, ClockedChip, HIGH, LOW, Pin, Voltage } from "../../chip.js";
|
||||
|
||||
export class Bit extends ClockedChip {
|
||||
bit: Voltage = LOW;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(["in", "load"], ["out"], name, [], ["in", "load"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("load").voltage() === HIGH) {
|
||||
this.bit = this.in().voltage();
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().pull(this.bit ?? 0);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bit = LOW;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private register: { bits: number },
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.register.bits & 0xffff;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.register.bits = num & 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
export class Register extends ClockedChip {
|
||||
bits = 0x00;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(["in[16]", "load"], ["out[16]"], name, [], ["in", "load"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("load").voltage() === HIGH) {
|
||||
this.bits = this.in().busVoltage & 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this.bits & 0xffff;
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
return name === this.name
|
||||
? new RegisterBus(this.name, this)
|
||||
: super.get(name, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bits = 0x00;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class VRegister extends Register {}
|
||||
|
||||
export class PC extends ClockedChip {
|
||||
bits = 0x00;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(
|
||||
["in[16]", "reset", "load", "inc"],
|
||||
["out[16]"],
|
||||
name,
|
||||
[],
|
||||
["in", "reset", "load", "inc"],
|
||||
);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("reset").voltage() === HIGH) {
|
||||
this.bits = 0;
|
||||
} else if (this.in("load").voltage() === HIGH) {
|
||||
this.bits = this.in().busVoltage & 0xffff;
|
||||
} else if (this.in("inc").voltage() === HIGH) {
|
||||
this.bits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this.bits & 0xffff;
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
return name === this.name
|
||||
? new RegisterBus(this.name, this)
|
||||
: super.get(name, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bits = 0x00;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ClockedChip } from "../../chip.js";
|
||||
|
||||
export class DFF extends ClockedChip {
|
||||
constructor(name?: string) {
|
||||
super(["in"], ["out"], name, ["t"], ["in"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
// Read in into t
|
||||
const t = this.in().voltage();
|
||||
this.pin("t").pull(t);
|
||||
}
|
||||
|
||||
override tock() {
|
||||
// write t into out
|
||||
const t = this.pin("t").voltage();
|
||||
this.out().pull(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Memory, Memory as MemoryChip } from "../../../cpu/memory.js";
|
||||
import { Bus, ClockedChip, Pin } from "../../chip.js";
|
||||
|
||||
export class RAM extends ClockedChip {
|
||||
protected _memory: MemoryChip;
|
||||
private _nextData = 0;
|
||||
private _address = 0;
|
||||
|
||||
get memory() {
|
||||
return this._memory;
|
||||
}
|
||||
get address() {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly width: number,
|
||||
name?: string,
|
||||
) {
|
||||
super(
|
||||
["in[16]", "load", `address[${width}]`],
|
||||
[`out[16]`],
|
||||
name,
|
||||
[],
|
||||
["in", "load"],
|
||||
);
|
||||
this._memory = new MemoryChip(Math.pow(2, this.width));
|
||||
}
|
||||
|
||||
override tick() {
|
||||
const load = this.in("load").voltage();
|
||||
this._address = this.in("address").busVoltage;
|
||||
if (load) {
|
||||
this._nextData = this.in().busVoltage;
|
||||
this._memory.set(this._address, this._nextData);
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this._memory?.get(this._address) ?? 0;
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const address = this.in("address").busVoltage;
|
||||
this.out().busVoltage = this._memory?.get(address) ?? 0;
|
||||
}
|
||||
|
||||
at(idx: number): Pin {
|
||||
assert(
|
||||
idx < this._memory.size,
|
||||
() => `Request out of bounds (${idx} >= ${this._memory.size})`,
|
||||
);
|
||||
return new RamBus(`${this.name}[${idx}]`, idx, this._memory);
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number) {
|
||||
return name === this.name ? this.at(offset ?? 0) : super.get(name);
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this._memory.reset();
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class RamBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly index: number,
|
||||
private ram: Memory,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.ram.get(this.index);
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.ram.set(this.index, num);
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM8 extends RAM {
|
||||
constructor() {
|
||||
super(3, "RAM8");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM64 extends RAM {
|
||||
constructor() {
|
||||
super(6, "RAM64");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM512 extends RAM {
|
||||
constructor() {
|
||||
super(9, "RAM512");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM4K extends RAM {
|
||||
constructor() {
|
||||
super(12, "RAM4K");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM16K extends RAM {
|
||||
constructor() {
|
||||
super(14, "RAM16K");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { And, Inc16, Mux16, Not, Not16, Or, Xor } from "./builtins/index.js";
|
||||
import { Nand } from "./builtins/logic/nand.js";
|
||||
import { Bit, PC, Register } from "./builtins/sequential/bit.js";
|
||||
import { DFF } from "./builtins/sequential/dff.js";
|
||||
import {
|
||||
Bus,
|
||||
Chip,
|
||||
ConstantBus,
|
||||
HIGH,
|
||||
InSubBus,
|
||||
LOW,
|
||||
OutSubBus,
|
||||
parseToPin,
|
||||
printChip,
|
||||
TRUE_BUS,
|
||||
} from "./chip.js";
|
||||
import { Clock } from "./clock.js";
|
||||
|
||||
describe("Chip", () => {
|
||||
it("parses toPin", () => {
|
||||
expect(parseToPin("a")).toMatchObject({ pin: "a" });
|
||||
expect(parseToPin("a[2]")).toMatchObject({ pin: "a", start: 2 });
|
||||
expect(parseToPin("a[2..4]")).toMatchObject({
|
||||
pin: "a",
|
||||
start: 2,
|
||||
end: 4,
|
||||
});
|
||||
});
|
||||
|
||||
describe("combinatorial", () => {
|
||||
describe("nand", () => {
|
||||
it("can eval a nand gate", () => {
|
||||
const nand = new Nand();
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in("a")?.pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in("b")?.pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(LOW);
|
||||
|
||||
nand.in("a")?.pull(LOW);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("not", () => {
|
||||
it("evaluates a not gate", () => {
|
||||
const notChip = new Not();
|
||||
|
||||
notChip.eval();
|
||||
expect(notChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
notChip.in().pull(HIGH);
|
||||
notChip.eval();
|
||||
expect(notChip.out().voltage()).toBe(LOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and", () => {
|
||||
it("evaluates an and gate", () => {
|
||||
const andChip = new And();
|
||||
|
||||
const a = assertExists(andChip.in("a"));
|
||||
const b = assertExists(andChip.in("b"));
|
||||
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
|
||||
b.pull(HIGH);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
a.pull(LOW);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe("or", () => {
|
||||
it("evaluates an or gate", () => {
|
||||
const orChip = new Or();
|
||||
|
||||
const a = assertExists(orChip.in("a"));
|
||||
const b = assertExists(orChip.in("b"));
|
||||
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
orChip.eval();
|
||||
printChip(orChip);
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
b.pull(HIGH);
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
a.pull(LOW);
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("xor", () => {
|
||||
it("evaluates an xor gate", () => {
|
||||
const xorChip = new Xor();
|
||||
|
||||
const a = assertExists(xorChip.in("a"));
|
||||
const b = assertExists(xorChip.in("b"));
|
||||
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
b.pull(HIGH);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(LOW);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("wide", () => {
|
||||
describe("Not16", () => {
|
||||
it("evaluates a not16 gate", () => {
|
||||
const not16 = new Not16();
|
||||
|
||||
const inn = not16.in();
|
||||
|
||||
inn.busVoltage = 0x0;
|
||||
not16.eval();
|
||||
expect(not16.out().busVoltage).toBe(0xffff);
|
||||
|
||||
inn.busVoltage = 0xf00f;
|
||||
not16.eval();
|
||||
expect(not16.out().busVoltage).toBe(0x0ff0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bus voltage", () => {
|
||||
it("sets and returns wide busses", () => {
|
||||
const pin = new Bus("wide", 16);
|
||||
pin.busVoltage = 0xf00f;
|
||||
expect(pin.voltage(0)).toBe(1);
|
||||
expect(pin.voltage(8)).toBe(0);
|
||||
expect(pin.voltage(9)).toBe(0);
|
||||
expect(pin.voltage(15)).toBe(1);
|
||||
expect(pin.busVoltage).toBe(0xf00f);
|
||||
});
|
||||
|
||||
it("creates wide busses internally", () => {
|
||||
const chip = new Chip([], [], "WithWide");
|
||||
|
||||
chip.wire(new Not16(), [
|
||||
{
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
|
||||
const width = chip.pins.get("a")?.width;
|
||||
expect(width).toBe(16);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and16", () => undefined);
|
||||
});
|
||||
|
||||
describe("SubBus", () => {
|
||||
class Not3 extends Chip {
|
||||
constructor() {
|
||||
super(["in[3]"], ["out[3]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const out = ~inn & 0b111;
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
it("drives OutSubBus", () => {
|
||||
const notChip = new Not();
|
||||
const inPin = new Bus("in", 3);
|
||||
const outSubBus = new OutSubBus(notChip.in(), 1, 1);
|
||||
inPin.connect(outSubBus);
|
||||
|
||||
inPin.busVoltage = 0b0;
|
||||
expect(notChip.in().busVoltage).toBe(0b0);
|
||||
inPin.busVoltage = 0b111;
|
||||
expect(notChip.in().busVoltage).toBe(0b1);
|
||||
});
|
||||
|
||||
it("wires SubBus in=in[1]", () => {
|
||||
const not3Chip = new Not3();
|
||||
const notPart = new Not();
|
||||
const inPin = not3Chip.in();
|
||||
|
||||
not3Chip.wire(notPart, [
|
||||
{
|
||||
from: { name: "in", start: 1, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
|
||||
inPin.busVoltage = 0b0;
|
||||
not3Chip.eval();
|
||||
expect(notPart.in().busVoltage).toBe(0b0);
|
||||
inPin.busVoltage = 0b111;
|
||||
not3Chip.eval();
|
||||
expect(notPart.in().busVoltage).toBe(0b1);
|
||||
});
|
||||
|
||||
it("wires SubBus in[0]=a", () => {
|
||||
const chip = new Chip(["a", "b"], ["out[3]"]);
|
||||
const not3 = new Not3();
|
||||
|
||||
// Not3(in[0]=a, in[1]=b, in[2]=b, out=out)
|
||||
chip.wire(not3, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: undefined },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: undefined },
|
||||
to: { name: "in", start: 1, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: undefined },
|
||||
to: { name: "in", start: 2, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: undefined },
|
||||
to: { name: "out", start: 0, width: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
assertExists(chip.in("b")).busVoltage = 1;
|
||||
assertExists(chip.in("a")).busVoltage = 0;
|
||||
chip.eval();
|
||||
expect(chip.out().busVoltage).toBe(0b001);
|
||||
});
|
||||
|
||||
it("wires SubBus out=out[1]", () => {
|
||||
const threeChip = new (class ThreeChip extends Chip {
|
||||
constructor() {
|
||||
super([], ["out[3]"]);
|
||||
}
|
||||
})();
|
||||
|
||||
const notPart = new Not();
|
||||
threeChip.wire(notPart, [
|
||||
{
|
||||
from: { name: "out", start: 1, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
const outPin = notPart.out();
|
||||
|
||||
outPin.busVoltage = 0b0;
|
||||
expect(threeChip.out().busVoltage).toBe(0b0);
|
||||
outPin.busVoltage = 0b1;
|
||||
expect(threeChip.out().busVoltage).toBe(0b010);
|
||||
});
|
||||
|
||||
it("widens output busses if necessary", () => {
|
||||
const mux4way16 = new Chip(
|
||||
["in[16]", "b[16]", "c[16]", "d[16]", "sel[2]"],
|
||||
["out[16]"],
|
||||
);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "a", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 0, width: 1 },
|
||||
to: { name: "sel", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out1", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "c", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "d", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 0, width: 1 },
|
||||
to: { name: "sel", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out2", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "out1", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out2", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 1, width: 1 },
|
||||
to: { name: "sel", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("widens internal busses if necessary", () => {
|
||||
const chip = new Chip(["in"], [], "test", ["t"]);
|
||||
|
||||
chip.wire(new Not(), [
|
||||
{
|
||||
from: { name: "in", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "t", start: 1, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
|
||||
chip.in().busVoltage = 0b0;
|
||||
chip.eval();
|
||||
expect(chip.pin("t").busVoltage).toBe(0b10);
|
||||
});
|
||||
|
||||
class Not8 extends Chip {
|
||||
constructor() {
|
||||
super(["in[8]"], ["out[8]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const out = ~inn & 0xff;
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
it("assigns input inside wide busses", () => {
|
||||
class Foo extends Chip {
|
||||
readonly not8 = new Not8();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.parts.push(this.not8);
|
||||
this.pins.insert(new ConstantBus("pal", 0b1010_1100_0011_0101));
|
||||
this.pins.get("pal")?.connect(new OutSubBus(this.not8.in(), 4, 8));
|
||||
this.pins.emplace("out1", 5);
|
||||
const out1Bus = new OutSubBus(
|
||||
assertExists(this.pins.get("out1")),
|
||||
3,
|
||||
5,
|
||||
);
|
||||
this.not8.out().connect(out1Bus);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
foo.eval();
|
||||
expect(foo.not8.in().busVoltage).toEqual(0b1100_0011);
|
||||
expect(foo.pin("out1")?.busVoltage).toEqual(0b00111);
|
||||
});
|
||||
|
||||
it("assigns output inside wide busses", () => {
|
||||
// From figure A2.2, page 287, 2nd edition
|
||||
class Foo extends Chip {
|
||||
readonly not8 = new Not8();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.parts.push(this.not8);
|
||||
this.pins.insert(new ConstantBus("six", 0b110));
|
||||
// in[0..1] = true
|
||||
TRUE_BUS.connect(new InSubBus(this.not8.in(), 0, 2));
|
||||
// in[3..5] = six, 110
|
||||
this.pins.get("six")?.connect(new InSubBus(this.not8.in(), 3, 3));
|
||||
// in[7] = true
|
||||
TRUE_BUS.connect(new InSubBus(this.not8.in(), 7, 1));
|
||||
// out[3..7] = out1
|
||||
this.pins.emplace("out1", 5);
|
||||
const out1Bus = new OutSubBus(
|
||||
assertExists(this.pins.get("out1")),
|
||||
3,
|
||||
5,
|
||||
);
|
||||
this.not8.out().connect(out1Bus);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
foo.eval();
|
||||
expect(foo.not8.in().busVoltage).toBe(0b10110011);
|
||||
expect(foo.pin("out1").busVoltage).toBe(0b01001);
|
||||
});
|
||||
|
||||
it("pulls portions of true", () => {
|
||||
class Foo extends Chip {
|
||||
readonly chip = new Not3();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.wire(this.chip, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 1, width: 2 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
|
||||
const inVoltage = foo.chip.in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b110));
|
||||
});
|
||||
|
||||
it("pulls start of true", () => {
|
||||
class Foo extends Chip {
|
||||
readonly chip = new Not3();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.wire(this.chip, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 2 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
|
||||
const inVoltage = foo.chip.in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b11));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sequential", () => {
|
||||
const clock = Clock.get();
|
||||
beforeEach(() => {
|
||||
clock.reset();
|
||||
});
|
||||
|
||||
describe("dff", () => {
|
||||
it("flips and flops", () => {
|
||||
clock.reset();
|
||||
const dff = new DFF();
|
||||
|
||||
clock.tick(); // Read input, low
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
clock.tock(); // Write t, low
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
|
||||
dff.in().pull(HIGH);
|
||||
clock.tick(); // Read input, HIGH
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
clock.tock(); // Write t, HIGH
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
|
||||
clock.tick();
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
clock.tock();
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bit", () => {
|
||||
it("does not update when load is low", () => {
|
||||
clock.reset();
|
||||
const bit = new Bit();
|
||||
|
||||
const inn = bit.in();
|
||||
const load = bit.in("load");
|
||||
const out = bit.out();
|
||||
|
||||
load.pull(LOW);
|
||||
inn.pull(HIGH);
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tick();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tock();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
});
|
||||
|
||||
it("does updates when load is high", () => {
|
||||
clock.reset();
|
||||
const bit = new Bit();
|
||||
|
||||
const inn = bit.in();
|
||||
const load = bit.in("load");
|
||||
const out = bit.out();
|
||||
|
||||
load.pull(HIGH);
|
||||
inn.pull(HIGH);
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tick();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tock();
|
||||
expect(out.voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PC", () => {
|
||||
it("remains constant when not ticking", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
});
|
||||
|
||||
it("increments when ticking", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
pc.in("inc").pull(HIGH);
|
||||
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(1);
|
||||
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(1);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(2);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
clock.eval();
|
||||
expect(out.busVoltage).toBe(i + 3);
|
||||
}
|
||||
});
|
||||
|
||||
it("loads a jump value", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
pc.in().busVoltage = 0x8286;
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
|
||||
pc.in("load").pull(HIGH);
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.eval();
|
||||
expect(out.busVoltage).toBe(0x8286);
|
||||
});
|
||||
|
||||
it("resets", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
pc.in("inc").pull(HIGH);
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
clock.eval();
|
||||
}
|
||||
|
||||
expect(out.busVoltage).toBe(10);
|
||||
|
||||
pc.in("reset").pull(HIGH);
|
||||
|
||||
clock.eval();
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sorts parts before eval", () => {
|
||||
class FooA extends Chip {
|
||||
readonly notA = new Not();
|
||||
readonly notB = new Not();
|
||||
constructor() {
|
||||
super([], ["out"], "Foo", ["x"]);
|
||||
this.wire(this.notA, [
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notB, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const fooA = new FooA();
|
||||
fooA.sortParts();
|
||||
expect(fooA.parts).toEqual([fooA.notB, fooA.notA]);
|
||||
|
||||
class FooB extends Chip {
|
||||
readonly notA = new Not();
|
||||
readonly notB = new Not();
|
||||
constructor() {
|
||||
super([], ["out"], "Foo", ["x"]);
|
||||
this.wire(this.notA, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notB, [
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const fooB = new FooB();
|
||||
fooB.sortParts();
|
||||
expect(fooB.parts).toEqual([fooB.notA, fooB.notB]);
|
||||
});
|
||||
|
||||
it("sorts clocked chips", () => {
|
||||
class FooC extends Chip {
|
||||
readonly register = new Register();
|
||||
readonly inc16A = new Inc16();
|
||||
readonly inc16B = new Inc16();
|
||||
constructor() {
|
||||
super([], [], "Foo", []);
|
||||
this.wire(this.inc16B, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "c", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.register, [
|
||||
{
|
||||
from: { name: "c", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "load", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.inc16A, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
const fooC = new FooC();
|
||||
fooC.sortParts();
|
||||
const parts = fooC.parts.map((chip) => chip.id);
|
||||
expect(parts).toEqual([fooC.register.id, fooC.inc16A.id, fooC.inc16B.id]);
|
||||
});
|
||||
|
||||
it("evals without order issues (after sorting)", () => {
|
||||
/*
|
||||
CHIP Or {
|
||||
IN a, b;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
|
||||
Not(in =b , out = net2);
|
||||
Nand(a = net, b =net2 , out =out );
|
||||
Not(in =a , out = net);
|
||||
}
|
||||
*/
|
||||
class OrA extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrA", []);
|
||||
this.wire(this.nota, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.nand, [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notb, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.sortParts();
|
||||
}
|
||||
}
|
||||
|
||||
class OrB extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrB", []);
|
||||
this.wire(this.nota, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notb, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.nand, [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class OrC extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrC", []);
|
||||
this.wireAll([
|
||||
{
|
||||
part: this.nota,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
part: this.nand,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
part: this.notb,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const ora = new OrA();
|
||||
ora.in("a").pull(HIGH);
|
||||
ora.in("b").pull(LOW);
|
||||
ora.eval();
|
||||
expect(ora.out("out").busVoltage).toBe(HIGH);
|
||||
|
||||
const orb = new OrB();
|
||||
orb.in("a").pull(HIGH);
|
||||
orb.in("b").pull(LOW);
|
||||
orb.eval();
|
||||
expect(orb.out("out").busVoltage).toBe(HIGH);
|
||||
|
||||
const orc = new OrC();
|
||||
orc.in("a").pull(HIGH);
|
||||
orc.in("b").pull(LOW);
|
||||
orc.eval();
|
||||
expect(orc.out("out").busVoltage).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,830 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { assert, assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import type { Subscription } from "rxjs";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { Clock } from "./clock.js";
|
||||
|
||||
export const HIGH = 1;
|
||||
export const LOW = 0;
|
||||
export type Voltage = typeof HIGH | typeof LOW;
|
||||
|
||||
export interface Pin {
|
||||
readonly name: string;
|
||||
readonly width: number;
|
||||
busVoltage: number;
|
||||
pull(voltage: Voltage, bit?: number): void;
|
||||
toggle(bit?: number): void;
|
||||
voltage(bit?: number): Voltage;
|
||||
connect(pin: Pin): void;
|
||||
}
|
||||
|
||||
export function isConstantPin(pinName: string): boolean {
|
||||
return (
|
||||
pinName === "false" ||
|
||||
pinName === "true" ||
|
||||
pinName === "0" ||
|
||||
pinName === "1"
|
||||
);
|
||||
}
|
||||
|
||||
export class Bus implements Pin {
|
||||
state: Voltage[];
|
||||
next: Pin[] = [];
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly width = 1,
|
||||
) {
|
||||
this.state = range(0, this.width).map(() => LOW);
|
||||
}
|
||||
|
||||
ensureWidth(newWidth: number) {
|
||||
assert(newWidth <= 16, `Cannot widen past 16 to ${newWidth} bits`);
|
||||
if (this.width < newWidth) {
|
||||
(this as { width: number }).width = newWidth;
|
||||
this.state = [
|
||||
...this.state,
|
||||
...range(this.width, newWidth).map(() => LOW as Voltage),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
connect(next: Pin) {
|
||||
this.next.push(next);
|
||||
next.busVoltage = this.busVoltage;
|
||||
}
|
||||
|
||||
pull(voltage: Voltage, bit = 0) {
|
||||
assert(
|
||||
bit >= 0 && bit < this.width,
|
||||
`Bit out of bounds: ${this.name}@${bit}`,
|
||||
);
|
||||
this.state[bit] = voltage;
|
||||
this.next.forEach((n) => n.pull(voltage, bit));
|
||||
}
|
||||
|
||||
voltage(bit = 0): Voltage {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
return this.state[bit];
|
||||
}
|
||||
|
||||
set busVoltage(voltage: number) {
|
||||
for (const i of range(0, this.width)) {
|
||||
this.state[i] = ((voltage & (1 << i)) >> i) as Voltage;
|
||||
}
|
||||
this.next.forEach((n) => (n.busVoltage = this.busVoltage));
|
||||
}
|
||||
|
||||
get busVoltage(): number {
|
||||
return range(0, this.width).reduce((b, i) => b | (this.state[i] << i), 0);
|
||||
}
|
||||
|
||||
toggle(bit = 0) {
|
||||
const nextVoltage = this.voltage(bit) === LOW ? HIGH : LOW;
|
||||
this.pull(nextVoltage, bit);
|
||||
}
|
||||
}
|
||||
|
||||
export class InSubBus extends Bus {
|
||||
constructor(
|
||||
private bus: Pin,
|
||||
private start: number,
|
||||
override readonly width = 1,
|
||||
) {
|
||||
super(bus.name);
|
||||
assert(
|
||||
start >= 0 && start + width <= bus.width,
|
||||
`Mismatched InSubBus dimensions on ${bus.name} (${width} + ${start} > ${bus.width})`,
|
||||
);
|
||||
this.connect(bus);
|
||||
}
|
||||
|
||||
override pull(voltage: Voltage, bit = 0) {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
this.bus.pull(voltage, this.start + bit);
|
||||
}
|
||||
|
||||
override voltage(bit = 0): Voltage {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
return this.bus.voltage(this.start + bit);
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
const high = this.bus.busVoltage & ~mask(this.width + this.start);
|
||||
const low = this.bus.busVoltage & mask(this.start);
|
||||
const mid = (voltage & mask(this.width)) << this.start;
|
||||
this.bus.busVoltage = high | mid | low;
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return (this.bus.busVoltage >> this.start) & mask(this.width);
|
||||
}
|
||||
|
||||
override connect(bus: Pin): void {
|
||||
assert(
|
||||
this.start + this.width <= bus.width,
|
||||
`Mismatched InSubBus connection dimensions (From ${bus.name} to ${this.name})`,
|
||||
);
|
||||
this.bus = bus;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutSubBus extends Bus {
|
||||
constructor(
|
||||
private bus: Pin,
|
||||
private start: number,
|
||||
override readonly width = 1,
|
||||
) {
|
||||
super(bus.name);
|
||||
assert(start >= 0 && width <= bus.width, `Mismatched OutSubBus dimensions`);
|
||||
this.connect(bus);
|
||||
}
|
||||
|
||||
override pull(voltage: Voltage, bit = 0) {
|
||||
if (bit >= this.start && bit < this.start + this.width) {
|
||||
this.bus.pull(voltage, bit - this.start);
|
||||
}
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
this.bus.busVoltage =
|
||||
(voltage & mask(this.width + this.start)) >> this.start;
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return (this.bus.busVoltage >> this.start) & mask(this.width);
|
||||
}
|
||||
|
||||
override connect(bus: Pin): void {
|
||||
assert(
|
||||
this.width <= bus.width,
|
||||
`Mismatched OutSubBus connection dimensions`,
|
||||
);
|
||||
this.bus = bus;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConstantBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly value: number,
|
||||
) {
|
||||
super(name, 16 /* TODO: get high bit index */);
|
||||
}
|
||||
|
||||
pullHigh(_ = 0) {
|
||||
return undefined;
|
||||
}
|
||||
pullLow(_ = 0) {
|
||||
return undefined;
|
||||
}
|
||||
override voltage(_ = 0): Voltage {
|
||||
return (this.busVoltage & 0x1) as Voltage;
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
// Noop
|
||||
}
|
||||
override get busVoltage(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export const TRUE_BUS = new ConstantBus("true", 0xffff);
|
||||
export const FALSE_BUS = new ConstantBus("false", 0);
|
||||
|
||||
export function parsePinDecl(toPin: string): {
|
||||
pin: string;
|
||||
width: number;
|
||||
} {
|
||||
const { pin, w } = toPin.match(/(?<pin>[a-zA-Z]+)(\[(?<w>\d+)\])?/)
|
||||
?.groups as {
|
||||
pin: string;
|
||||
w?: string;
|
||||
};
|
||||
return {
|
||||
pin,
|
||||
width: w ? Number(w) : 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseToPin(toPin: string): {
|
||||
pin: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
} {
|
||||
const { pin, i, j } = toPin.match(
|
||||
/(?<pin>[a-z]+)(\[(?<i>\d+)(\.\.(?<j>\d+))?\])?/,
|
||||
)?.groups as { pin: string; i?: string; j?: string };
|
||||
return {
|
||||
pin,
|
||||
start: i ? Number(i) : undefined,
|
||||
end: j ? Number(j) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export class Pins {
|
||||
private readonly map = new Map<string, Pin>();
|
||||
|
||||
insert(pin: Pin) {
|
||||
const { name } = pin;
|
||||
assert(!this.map.has(name), `Pins already has ${name}!`);
|
||||
this.map.set(name, pin);
|
||||
}
|
||||
|
||||
emplace(name: string, minWidth?: number) {
|
||||
if (this.has(name)) {
|
||||
return assertExists(this.get(name));
|
||||
} else {
|
||||
const pin = new Bus(name, minWidth);
|
||||
this.insert(pin);
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
|
||||
has(pin: string): boolean {
|
||||
return this.map.has(pin);
|
||||
}
|
||||
|
||||
get(pin: string): Pin | undefined {
|
||||
return this.map.get(pin);
|
||||
}
|
||||
|
||||
entries(): Iterable<Pin> {
|
||||
return this.map.values();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
function validateWidth(
|
||||
start: number,
|
||||
width: number,
|
||||
pin: Pin,
|
||||
): Result<void, string> {
|
||||
return start + width <= pin.width
|
||||
? Ok()
|
||||
: Err(`Sub-bus index out of range (${pin.name} has width ${pin.width})`);
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
|
||||
export interface PartWireError {
|
||||
wireIndex: number;
|
||||
lhs: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WireError {
|
||||
message: string;
|
||||
lhs: boolean;
|
||||
}
|
||||
export class Chip {
|
||||
readonly id = id++;
|
||||
ins = new Pins();
|
||||
outs = new Pins();
|
||||
pins = new Pins();
|
||||
insToPart = new Map<string, Set<Chip>>();
|
||||
partToOuts = new Map<Chip, Set<string>>();
|
||||
parts: Chip[] = [];
|
||||
clockedPins: Set<string>;
|
||||
clockSubscription?: Subscription;
|
||||
|
||||
get clocked() {
|
||||
if (this.clockedPins.size > 0) {
|
||||
return true;
|
||||
}
|
||||
for (const part of this.parts) {
|
||||
if (part.clocked) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(
|
||||
ins: (string | { pin: string; width: number })[],
|
||||
outs: (string | { pin: string; width: number })[],
|
||||
public name?: string,
|
||||
internals: (string | { pin: string; width: number })[] = [],
|
||||
clocked: string[] = [],
|
||||
) {
|
||||
for (const inn of ins) {
|
||||
const { pin, width = 1 } =
|
||||
(inn as { pin: string }).pin !== undefined
|
||||
? (inn as { pin: string; width: number })
|
||||
: parsePinDecl(inn as string);
|
||||
this.ins.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
for (const out of outs) {
|
||||
const { pin, width = 1 } =
|
||||
(out as { pin: string }).pin !== undefined
|
||||
? (out as { pin: string; width: number })
|
||||
: parsePinDecl(out as string);
|
||||
this.outs.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
for (const internal of internals) {
|
||||
const { pin, width = 1 } =
|
||||
(internal as { pin: string }).pin !== undefined
|
||||
? (internal as { pin: string; width: number })
|
||||
: parsePinDecl(internal as string);
|
||||
this.pins.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
this.clockedPins = new Set(clocked);
|
||||
|
||||
this.subscribeToClock();
|
||||
}
|
||||
|
||||
subscribeToClock() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
this.clockSubscription = Clock.subscribe(() => this.eval());
|
||||
}
|
||||
|
||||
reset() {
|
||||
for (const [_, pin] of this.ins) {
|
||||
pin.busVoltage = 0;
|
||||
}
|
||||
for (const part of this.parts) {
|
||||
part.reset();
|
||||
}
|
||||
this.eval();
|
||||
}
|
||||
|
||||
in(pin = "in"): Pin {
|
||||
assert(this.hasIn(pin), `No in pin ${pin}`);
|
||||
return assertExists(this.ins.get(pin));
|
||||
}
|
||||
|
||||
out(pin = "out"): Pin {
|
||||
assert(this.hasOut(pin), `No in pin ${pin}`);
|
||||
return assertExists(this.outs.get(pin));
|
||||
}
|
||||
|
||||
hasIn(pin: string): boolean {
|
||||
return this.ins.has(pin);
|
||||
}
|
||||
|
||||
hasOut(pin: string): boolean {
|
||||
return this.outs.has(pin);
|
||||
}
|
||||
|
||||
pin(name: string): Pin {
|
||||
assert(this.pins.has(name), "Pin not available in ");
|
||||
return assertExists(this.pins.get(name));
|
||||
}
|
||||
|
||||
get(name: string, offset?: number): Pin | undefined {
|
||||
if (this.ins.has(name)) {
|
||||
return assertExists(this.ins.get(name));
|
||||
}
|
||||
if (this.outs.has(name)) {
|
||||
return assertExists(this.outs.get(name));
|
||||
}
|
||||
if (this.pins.has(name)) {
|
||||
return assertExists(this.pins.get(name));
|
||||
}
|
||||
return this.getBuiltin(name, offset);
|
||||
}
|
||||
|
||||
private getBuiltin(name: string, offset = 0): Pin | undefined {
|
||||
if (BUILTIN_NAMES.includes(name)) {
|
||||
for (const part of this.parts) {
|
||||
const pin = part.get(name, offset);
|
||||
if (pin) {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isInPin(pin: string): boolean {
|
||||
return this.ins.has(pin);
|
||||
}
|
||||
|
||||
isOutPin(pin: string): boolean {
|
||||
return this.outs.has(pin);
|
||||
}
|
||||
|
||||
isExternalPin(pin: string): boolean {
|
||||
return this.isInPin(pin) || this.isOutPin(pin) || isConstantPin(pin);
|
||||
}
|
||||
|
||||
isInternalPin(pin: string): boolean {
|
||||
return !this.isExternalPin(pin);
|
||||
}
|
||||
|
||||
pathExists(start: string, end: string) {
|
||||
const nodes: (Chip | string)[] = [start];
|
||||
|
||||
while (nodes.length > 0) {
|
||||
const node = assertExists(nodes.pop());
|
||||
if (typeof node == "string") {
|
||||
if (node == end) {
|
||||
return true;
|
||||
}
|
||||
nodes.push(...(this.insToPart.get(node) ?? []));
|
||||
} else {
|
||||
nodes.push(...(this.partToOuts.get(node) ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isClockedPin(pin: string) {
|
||||
if (this.isInPin(pin)) {
|
||||
return ![...this.outs].some(([out, _]) => this.pathExists(pin, out));
|
||||
} else {
|
||||
return ![...this.ins].some(([in_, _]) => this.pathExists(in_, pin));
|
||||
}
|
||||
}
|
||||
|
||||
hasConnection(from: Chip, to: Chip): boolean {
|
||||
return [...(this.partToOuts.get(from) ?? [])].some((pin) =>
|
||||
this.insToPart.get(pin)?.has(to),
|
||||
);
|
||||
}
|
||||
|
||||
wire(part: Chip, connections: Connection[]): Result<void, PartWireError> {
|
||||
for (let i = 0; i < connections.length; i++) {
|
||||
const { from, to } = connections[i];
|
||||
if (part.isOutPin(to.name)) {
|
||||
const result = this.wireOutPin(part, to, from);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
wireIndex: i,
|
||||
lhs: Err(result).lhs,
|
||||
message: Err(result).message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const result = this.wireInPin(part, to, from);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
wireIndex: i,
|
||||
lhs: Err(result).lhs,
|
||||
message: Err(result).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.parts.push(part);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
wireAll(wires: Iterable<{ part: Chip; connections: Connection[] }>) {
|
||||
for (const { part, connections } of wires) {
|
||||
this.wire(part, connections);
|
||||
}
|
||||
this.sortParts();
|
||||
}
|
||||
|
||||
// Returns whether the part connection graph has a loop. This should be called
|
||||
// after wiring pins, so that connections are sorted topologically to best
|
||||
// simulate non-order-dependent wiring. This can be handled manually (OrB),
|
||||
// by calling sortParts() after wiring (OrA), or by using wireAll for creating
|
||||
// wires (OrC).
|
||||
sortParts(): boolean {
|
||||
const sorted: Chip[] = [];
|
||||
const visited = new Set<Chip>();
|
||||
const visiting = new Set<Chip>();
|
||||
|
||||
type Node = { part: Chip; isReturning: boolean };
|
||||
|
||||
const stack: Node[] = this.parts.map((part) => ({
|
||||
part,
|
||||
isReturning: false,
|
||||
}));
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = assertExists(stack.pop());
|
||||
|
||||
if (node.isReturning) {
|
||||
// If we are returning to this node, we can safely add it to the sorted list
|
||||
visited.add(node.part);
|
||||
sorted.push(node.part);
|
||||
} else if (!visited.has(node.part)) {
|
||||
if (visiting.has(node.part)) {
|
||||
return true;
|
||||
}
|
||||
visiting.add(node.part);
|
||||
|
||||
// Re-push this node to handle it on return
|
||||
stack.push({ part: node.part, isReturning: true });
|
||||
|
||||
// Push all its children to visit them
|
||||
for (const out of this.partToOuts.get(node.part) ?? []) {
|
||||
stack.push(
|
||||
...Array.from(this.insToPart.get(out) ?? [])
|
||||
.filter((part) => !visited.has(part))
|
||||
.map((part) => ({
|
||||
part,
|
||||
isReturning: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parts = sorted.reverse();
|
||||
return false;
|
||||
}
|
||||
|
||||
private findPin(from: string, minWidth?: number): Pin {
|
||||
if (from === "true" || from === "1") {
|
||||
return TRUE_BUS;
|
||||
}
|
||||
if (from === "false" || from === "0") {
|
||||
return FALSE_BUS;
|
||||
}
|
||||
if (this.ins.has(from)) {
|
||||
return assertExists(this.ins.get(from));
|
||||
}
|
||||
if (this.outs.has(from)) {
|
||||
return assertExists(this.outs.get(from));
|
||||
}
|
||||
return this.pins.emplace(from, minWidth);
|
||||
}
|
||||
|
||||
private wireOutPin(
|
||||
part: Chip,
|
||||
to: PinSide,
|
||||
from: PinSide,
|
||||
): Result<void, WireError> {
|
||||
const partPin = assertExists(
|
||||
part.outs.get(to.name),
|
||||
() => `Cannot wire to missing pin ${to.name}`,
|
||||
);
|
||||
to.width ??= partPin.width;
|
||||
|
||||
let chipPin = this.findPin(from.name, from.width ?? to.width);
|
||||
const isInternal = this.pins.has(chipPin.name);
|
||||
|
||||
from.width ??= chipPin.width;
|
||||
|
||||
if (chipPin instanceof ConstantBus) {
|
||||
return Err({
|
||||
message: `Cannot wire to constant bus`,
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Widen internal pins
|
||||
if (isInternal && chipPin instanceof Bus) {
|
||||
chipPin.ensureWidth(from.start + from.width);
|
||||
}
|
||||
|
||||
// Wrap the chipPin in an InBus when the chip side is dimensioned
|
||||
if (from.start > 0 || from.width !== chipPin.width) {
|
||||
const result = validateWidth(from.start, from.width, chipPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
chipPin = new InSubBus(chipPin, from.start, from.width);
|
||||
}
|
||||
|
||||
// Wrap the chipPin in an OutBus when the part side is dimensioned
|
||||
if (to.start > 0 || to.width !== partPin.width) {
|
||||
const result = validateWidth(to.start, to.width, partPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: false,
|
||||
});
|
||||
}
|
||||
chipPin = new OutSubBus(chipPin, to.start, to.width);
|
||||
}
|
||||
|
||||
if (!part.clockedPins.has(partPin.name)) {
|
||||
const partToOuts = this.partToOuts.get(part) ?? new Set();
|
||||
partToOuts.add(chipPin.name);
|
||||
this.partToOuts.set(part, partToOuts);
|
||||
}
|
||||
|
||||
const loop = this.sortParts();
|
||||
|
||||
if (loop) {
|
||||
const partToOuts = this.partToOuts.get(part) ?? new Set();
|
||||
partToOuts.delete(chipPin.name);
|
||||
this.partToOuts.set(part, partToOuts);
|
||||
return Err({ message: "Circular pin dependency", lhs: false });
|
||||
} else {
|
||||
partPin.connect(chipPin);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private wireInPin(
|
||||
part: Chip,
|
||||
to: PinSide,
|
||||
from: PinSide,
|
||||
): Result<void, WireError> {
|
||||
let partPin = assertExists(
|
||||
part.ins.get(to.name),
|
||||
() => `Cannot wire to missing pin ${to.name}`,
|
||||
);
|
||||
to.width ??= partPin.width;
|
||||
|
||||
const chipPin = this.findPin(from.name, from.width ?? to.width);
|
||||
|
||||
from.width ??= chipPin.width;
|
||||
|
||||
// Wrap the partPin in an InBus when the part side is dimensioned
|
||||
if (to.start > 0 || to.width !== partPin.width) {
|
||||
const result = validateWidth(to.start, to.width, partPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
partPin = new InSubBus(partPin, to.start, to.width);
|
||||
}
|
||||
|
||||
// Wrap the partPin in an OutBus when the chip side is dimensioned
|
||||
if (!["true", "false"].includes(chipPin.name)) {
|
||||
if (from.start > 0 || from.width !== chipPin.width) {
|
||||
const result = validateWidth(from.start, from.width, chipPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: false,
|
||||
});
|
||||
}
|
||||
partPin = new OutSubBus(partPin, from.start, from.width);
|
||||
}
|
||||
}
|
||||
|
||||
if (!part.clockedPins.has(partPin.name)) {
|
||||
const pinsToPart = this.insToPart.get(chipPin.name) ?? new Set();
|
||||
pinsToPart.add(part);
|
||||
this.insToPart.set(chipPin.name, pinsToPart);
|
||||
}
|
||||
|
||||
const loop = this.sortParts();
|
||||
|
||||
if (loop) {
|
||||
const pinsToPart = this.insToPart.get(chipPin.name) ?? new Set();
|
||||
pinsToPart.delete(part);
|
||||
this.insToPart.set(chipPin.name, pinsToPart);
|
||||
return Err({ message: "Circular pin dependency", lhs: true });
|
||||
} else {
|
||||
chipPin.connect(partPin);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
eval() {
|
||||
for (const chip of this.parts) {
|
||||
TRUE_BUS.next.forEach((pin) => (pin.busVoltage = TRUE_BUS.busVoltage));
|
||||
FALSE_BUS.next.forEach((pin) => (pin.busVoltage = FALSE_BUS.busVoltage));
|
||||
chip.eval();
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
tock() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
for (const part of this.parts) {
|
||||
part.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// For the ROM32K builtin to load from a file system
|
||||
async load(fs: FileSystem, path: string): Promise<void> {
|
||||
for (const part of this.parts) {
|
||||
if (part.name === "ROM32K") {
|
||||
await part.load(fs, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Low extends Chip {
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.outs.insert(FALSE_BUS);
|
||||
}
|
||||
}
|
||||
|
||||
export class High extends Chip {
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.outs.insert(TRUE_BUS);
|
||||
}
|
||||
}
|
||||
|
||||
export class ClockedChip extends Chip {
|
||||
override get clocked(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override subscribeToClock(): void {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
this.clockSubscription = Clock.subscribe(({ level }) => {
|
||||
if (level === LOW) {
|
||||
this.tock();
|
||||
} else {
|
||||
this.tick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override remove() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
super.remove();
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
super.reset();
|
||||
this.tick();
|
||||
this.tock();
|
||||
}
|
||||
}
|
||||
|
||||
export interface PinSide {
|
||||
name: string;
|
||||
start: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
// To is the part side
|
||||
to: PinSide;
|
||||
// From is the chip side
|
||||
from: PinSide;
|
||||
}
|
||||
|
||||
export type Pinout = Record<string, string>;
|
||||
export interface SerializedChip {
|
||||
id: number;
|
||||
name: string;
|
||||
ins: Pinout;
|
||||
outs: Pinout;
|
||||
pins: Pinout;
|
||||
children: SerializedChip[];
|
||||
}
|
||||
|
||||
function mask(width: number) {
|
||||
return Math.pow(2, width) - 1;
|
||||
}
|
||||
|
||||
function setBus(busses: Pinout, pin: Pin) {
|
||||
busses[pin.name] = bin(
|
||||
(pin.busVoltage & mask(pin.width)) <<
|
||||
(pin as unknown as { start: number }).start,
|
||||
);
|
||||
return busses;
|
||||
}
|
||||
|
||||
export function printChip(chip: Chip): SerializedChip {
|
||||
return {
|
||||
id: chip.id,
|
||||
name: chip.name ?? chip.constructor.name,
|
||||
ins: [...chip.ins.entries()].reduce(setBus, {} as Pinout),
|
||||
outs: [...chip.outs.entries()].reduce(setBus, {} as Pinout),
|
||||
pins: [...chip.pins.entries()].reduce(setBus, {} as Pinout),
|
||||
children: [...chip.parts.values()].map(printChip),
|
||||
};
|
||||
}
|
||||
|
||||
export const BUILTIN_NAMES = [
|
||||
"Register",
|
||||
"ARegister",
|
||||
"DRegister",
|
||||
"PC",
|
||||
"RAM8",
|
||||
"RAM64",
|
||||
"RAM512",
|
||||
"RAM4K",
|
||||
"RAM16K",
|
||||
"ROM32K",
|
||||
"Screen",
|
||||
"Keyboard",
|
||||
"Memory",
|
||||
];
|
||||
@@ -0,0 +1,92 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
||||
import { HIGH, LOW, Voltage } from "./chip.js";
|
||||
|
||||
interface Tick {
|
||||
readonly level: Voltage;
|
||||
readonly ticks: number;
|
||||
}
|
||||
|
||||
let clock: Clock;
|
||||
export class Clock {
|
||||
private level: Voltage = LOW;
|
||||
private ticks = 0;
|
||||
|
||||
static get() {
|
||||
if (clock === undefined) {
|
||||
clock = new Clock();
|
||||
}
|
||||
return clock;
|
||||
}
|
||||
|
||||
static subscribe(observer: (value: Tick) => void) {
|
||||
return Clock.get().$.subscribe(observer);
|
||||
}
|
||||
|
||||
get isHigh(): boolean {
|
||||
return this.level === HIGH;
|
||||
}
|
||||
|
||||
get isLow(): boolean {
|
||||
return this.level === LOW;
|
||||
}
|
||||
|
||||
private subject = new BehaviorSubject<Tick>({
|
||||
level: this.level,
|
||||
ticks: this.ticks,
|
||||
});
|
||||
readonly frameSubject = new Subject<void>();
|
||||
readonly resetSubject = new Subject<void>();
|
||||
|
||||
readonly $: Observable<Tick> = this.subject;
|
||||
readonly frame$: Observable<void> = this.frameSubject;
|
||||
readonly reset$: Observable<void> = this.resetSubject;
|
||||
|
||||
private next() {
|
||||
this.subject.next({
|
||||
level: this.level,
|
||||
ticks: this.ticks,
|
||||
});
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
// private
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.level = LOW;
|
||||
this.ticks = 0;
|
||||
this.next();
|
||||
this.resetSubject.next();
|
||||
}
|
||||
|
||||
tick() {
|
||||
assert(this.level === LOW, "Can only tick up from LOW");
|
||||
this.level = HIGH;
|
||||
this.next();
|
||||
}
|
||||
|
||||
tock() {
|
||||
assert(this.level === HIGH, "Can only tock down from HIGH");
|
||||
this.level = LOW;
|
||||
this.ticks += 1;
|
||||
this.next();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.level === HIGH ? this.tock() : this.tick();
|
||||
}
|
||||
|
||||
eval() {
|
||||
this.tick();
|
||||
this.tock();
|
||||
}
|
||||
|
||||
frame() {
|
||||
this.frameSubject.next();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.ticks}${this.level === HIGH ? "+" : ""}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
These are the errors from the current builder.
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L212
|
||||
try {
|
||||
result = getSubBus(pinName);
|
||||
} catch (Exception e) {
|
||||
input.HDLError(pinName + " has an invalid sub bus specification");
|
||||
}
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L217
|
||||
if (result != null) {
|
||||
if (result[0] < 0 || result[1] < 0)
|
||||
input.HDLError(pinName + ": negative bit numbers are illegal");
|
||||
else if (result[0] > result[1])
|
||||
input.HDLError(pinName + ": left bit number should be lower than the right one");
|
||||
else if (result[1] >= busWidth)
|
||||
input.HDLError(pinName + ": the specified sub bus is not in the bus range");
|
||||
}
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L274
|
||||
// find left pin info. If doesn't exist - error.
|
||||
byte leftType = partGateClass.getPinType(leftName);
|
||||
if (leftType == UNKNOWN_PIN_TYPE)
|
||||
input.HDLError(leftName + " is not a pin in " + partName);
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L310
|
||||
if ((rightType == UNKNOWN_PIN_TYPE || rightType == INTERNAL_PIN_TYPE) &&
|
||||
!fullRightName.equals(rightName))
|
||||
input.HDLError(fullRightName + ": sub bus of an internal node may not be used");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L333
|
||||
if (selfFittingWidth) {
|
||||
if(!rightName.equals(fullRightName))
|
||||
input.HDLError(rightName + " may not be subscripted");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L346
|
||||
// check that right & left has the same width
|
||||
if (leftWidth != rightWidth)
|
||||
input.HDLError(leftName + "(" + leftWidth + ") and " + rightName + "(" + rightWidth +
|
||||
") have different bus widths");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L352
|
||||
// make sure that an internal pin is only fed once by a part's output pin
|
||||
if ((rightType == INTERNAL_PIN_TYPE) && (leftType == OUTPUT_PIN_TYPE)) {
|
||||
if (rightPinInfo.isInitialized(rightSubBus))
|
||||
input.HDLError("An internal pin may only be fed once by a part's output pin");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L377
|
||||
// find connection type
|
||||
switch (leftType) {
|
||||
case INPUT_PIN_TYPE:
|
||||
switch (rightType) {
|
||||
case OUTPUT_PIN_TYPE:
|
||||
input.HDLError("Can't connect gate's output pin to part");
|
||||
|
||||
case OUTPUT_PIN_TYPE:
|
||||
switch (rightType) {
|
||||
case INPUT_PIN_TYPE:
|
||||
input.HDLError("Can't connect part's output pin to gate's input pin");
|
||||
@@ -0,0 +1,42 @@
|
||||
import { compare, compareLines, diff } from "./compare.js";
|
||||
|
||||
describe("compare", () => {
|
||||
it("diffs a row", () => {
|
||||
const as = ["a", "b", "c"];
|
||||
const bs = ["a", "d", "c"];
|
||||
const diffs = diff(as, bs);
|
||||
expect(diffs).toMatchObject([{ a: "b", b: "d", col: 1 }]);
|
||||
});
|
||||
|
||||
it("diffs a block", () => {
|
||||
const as = [
|
||||
["0", "0", "0"],
|
||||
["0", "1", "1"],
|
||||
["1", "0", "1"],
|
||||
["1", "1", "0"],
|
||||
];
|
||||
const bs = [
|
||||
["0", "0", "0"],
|
||||
["0", "1", "0"],
|
||||
["1", "0", "0"],
|
||||
["1", "1", "1"],
|
||||
];
|
||||
const diffs = compare(as, bs);
|
||||
expect(diffs).toMatchObject([
|
||||
{ a: "1", b: "0", row: 1, col: 2 },
|
||||
{ a: "1", b: "0", row: 2, col: 2 },
|
||||
{ a: "0", b: "1", row: 3, col: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareLines", () => {
|
||||
it("handles windows and unix lines", () => {
|
||||
expect(compareLines("AAA\r\nBBB\r\nCCC\r\n", "AAA\nBBB\nCCC\n")).toEqual(
|
||||
{},
|
||||
);
|
||||
expect(compareLines("AAA\nBBB\nCCC\n", "AAA\r\nBBB\r\nCCC\r\n")).toEqual(
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
export interface Diff {
|
||||
a: string;
|
||||
b: string;
|
||||
row?: number;
|
||||
col?: number;
|
||||
}
|
||||
|
||||
function normalLines(
|
||||
str: string,
|
||||
{
|
||||
trim = true,
|
||||
skipTrimmed = false,
|
||||
}: { trim?: boolean; skipTrimmed?: boolean } = {},
|
||||
): string[] {
|
||||
let lines = str.replace("\r\n", "\n").split("\n");
|
||||
|
||||
if (trim) lines = lines.map((line) => line.trim());
|
||||
if (skipTrimmed) lines = lines.filter((line) => line != "");
|
||||
return lines;
|
||||
}
|
||||
|
||||
export type CompareResultSuccess = Record<string, never>;
|
||||
export interface CompareResultLengths {
|
||||
lenA: number;
|
||||
lenB: number;
|
||||
}
|
||||
export interface CompareResultLine {
|
||||
line: number;
|
||||
}
|
||||
export type CompareResult =
|
||||
| CompareResultSuccess
|
||||
| CompareResultLine
|
||||
| CompareResultLengths;
|
||||
|
||||
export function compareLines(as: string, bs: string): CompareResult {
|
||||
const resultLines = normalLines(as);
|
||||
const compareLines = normalLines(bs);
|
||||
|
||||
if (resultLines.length != compareLines.length) {
|
||||
return { lenA: resultLines.length, lenB: compareLines.length };
|
||||
}
|
||||
|
||||
for (let line = 0; line < compareLines.length; line++) {
|
||||
if (resultLines[line] !== compareLines[line]) {
|
||||
return { line };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function compare(as: string[][], bs: string[][]): Diff[] {
|
||||
let diffs: Diff[] = [];
|
||||
|
||||
const q = Math.max(as.length, bs.length);
|
||||
for (let row = 0; row < q; row++) {
|
||||
const a = as[row] ?? [];
|
||||
const b = bs[row] ?? [];
|
||||
diffs = diffs.concat(
|
||||
diff(a, b).map((diff) => {
|
||||
diff.row = row;
|
||||
return diff;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}
|
||||
|
||||
export function diff(as: string[], bs: string[]): Diff[] {
|
||||
const diffs: Diff[] = [];
|
||||
|
||||
const q = Math.max(as.length, bs.length);
|
||||
for (let col = 0; col < q; col++) {
|
||||
const a = as[col] ?? "";
|
||||
const b = bs[col] ?? "";
|
||||
if (a !== b && !a.match(/\*+/)) {
|
||||
diffs.push({ a, b, col });
|
||||
}
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { alu, alua, COMMANDS, Flags } from "./alu.js";
|
||||
|
||||
describe("alu", () => {
|
||||
it("calculates", () => {
|
||||
expect(alu(COMMANDS.asm["0"], 123, 456)).toEqual([0, Flags.Zero]);
|
||||
expect(alu(COMMANDS.asm["D+A"], 123, 456)).toEqual([579, Flags.Positive]);
|
||||
expect(alu(COMMANDS.asm["D-A"], 123, 456)).toEqual([
|
||||
-333 & 0xffff,
|
||||
Flags.Negative,
|
||||
]);
|
||||
expect(alu(COMMANDS.asm["A-D"], 123, 456)).toEqual([333, Flags.Positive]);
|
||||
expect(alu(COMMANDS.asm["D&A"], 0b1010, 0b1101)).toEqual([
|
||||
0b1000,
|
||||
Flags.Positive,
|
||||
]);
|
||||
expect(alu(COMMANDS.asm["D|A"], 0b1010, 0b1101)).toEqual([
|
||||
0b1111,
|
||||
Flags.Positive,
|
||||
]);
|
||||
});
|
||||
|
||||
it("calculates undocumented", () => {
|
||||
// https://medium.com/@MadOverlord/14-nand2tetris-opcodes-they-dont-want-you-to-know-about-f3246831d1d1
|
||||
|
||||
// -2
|
||||
expect(alua(0b111110, 0, 0)).toEqual([0xfffe, Flags.Negative]);
|
||||
|
||||
// NAND
|
||||
expect(alua(0b000001, 0, 0)).toEqual([0xffff, Flags.Negative]);
|
||||
expect(alua(0b000001, 0, 1)).toEqual([0xffff, Flags.Negative]);
|
||||
expect(alua(0b000001, 1, 0)).toEqual([0xffff, Flags.Negative]);
|
||||
expect(alua(0b000001, 1, 1)).toEqual([0xfffe, Flags.Negative]);
|
||||
expect(alua(0b000001, 0b0011, 0b0101)).toEqual([
|
||||
0b1111111111111110,
|
||||
Flags.Negative,
|
||||
]);
|
||||
|
||||
// NOR
|
||||
expect(alua(0b010100, 0, 0)).toEqual([0xffff, Flags.Negative]);
|
||||
expect(alua(0b010100, 0, 1)).toEqual([0xfffe, Flags.Negative]);
|
||||
expect(alua(0b010100, 1, 0)).toEqual([0xfffe, Flags.Negative]);
|
||||
expect(alua(0b010100, 1, 1)).toEqual([0xfffe, Flags.Negative]);
|
||||
expect(alua(0b010100, 0b0011, 0b0101)).toEqual([
|
||||
0b1111_1111_1111_1000,
|
||||
Flags.Negative,
|
||||
]);
|
||||
|
||||
// Weird
|
||||
// 010000 : NOT(X) AND Y : 00,01,10,11 TruthTable=0100
|
||||
expect(alua(0b010000, 0b0011, 0b0101)[0] & 0b1111).toBe(0b0100);
|
||||
// 010001 : NOT(NOT(X) AND Y) : 00,01,10,11 TruthTable=1011
|
||||
expect(alua(0b010001, 0b0011, 0b0101)[0] & 0b1111).toBe(0b1011);
|
||||
// 000101 : NOT(X AND NOT(Y)) : 00,01,10,11 TruthTable=1101
|
||||
expect(alua(0b000101, 0b0011, 0b0101)[0] & 0b1111).toBe(0b1101);
|
||||
// 000100 : X AND NOT(Y) : 00,01,10,11 TruthTable=0010
|
||||
expect(alua(0b000100, 0b0011, 0b0101)[0] & 0b1111).toBe(0b0010);
|
||||
|
||||
// Bizarre
|
||||
expect(alua(0b010111, 13, 19)[0]).toBe(33); // X + Y + 1
|
||||
expect(alua(0b000110, 13, 19)[0]).toBe(-7 & 0xffff); // X — Y — 1
|
||||
expect(alua(0b011110, 13, 19)[0]).toBe(-15 & 0xffff); // -(X + 2)
|
||||
expect(alua(0b110110, 13, 19)[0]).toBe(-21 & 0xffff); // -(Y + 2)
|
||||
expect(alua(0b010110, 13, 19)[0]).toBe(-34 & 0xffff); // -(X + Y + 2)
|
||||
expect(alua(0b000011, 13, 19)[0]).toBe(-33 & 0xffff); // -(X + Y + 1)
|
||||
expect(alua(0b010010, 13, 19)[0]).toBe(5 & 0xffff); // -(X — Y + 1)
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,330 @@
|
||||
const commandASMValues = new Set([
|
||||
"0",
|
||||
"1",
|
||||
"-1",
|
||||
"D",
|
||||
"A",
|
||||
"!D",
|
||||
"!A",
|
||||
"-D",
|
||||
"-A",
|
||||
"D+1",
|
||||
"A+1",
|
||||
"D-1",
|
||||
"A-1",
|
||||
"D+A",
|
||||
"D-A",
|
||||
"A-D",
|
||||
"D&A",
|
||||
"D|A",
|
||||
] as const);
|
||||
|
||||
export type COMMANDS_ASM = typeof commandASMValues extends Set<infer S>
|
||||
? S
|
||||
: never;
|
||||
|
||||
export function isCommandAsm(command: string): command is COMMANDS_ASM {
|
||||
return (
|
||||
commandASMValues.has(command as COMMANDS_ASM) ||
|
||||
commandASMValues.has(command.replace("M", "A") as COMMANDS_ASM)
|
||||
);
|
||||
}
|
||||
|
||||
export type COMMANDS_OP =
|
||||
| 0b101010
|
||||
| 0b111111
|
||||
| 0b111010
|
||||
| 0b001100
|
||||
| 0b110000
|
||||
| 0b110000
|
||||
| 0b001101
|
||||
| 0b110001
|
||||
| 0b001111
|
||||
| 0b110011
|
||||
| 0b011111
|
||||
| 0b110111
|
||||
| 0b001110
|
||||
| 0b110010
|
||||
| 0b000010
|
||||
| 0b010011
|
||||
| 0b010011
|
||||
| 0b000111
|
||||
| 0b000000
|
||||
| 0b000000
|
||||
| 0b010101
|
||||
| 0b010101;
|
||||
|
||||
//Usefull for the visualization of the ALU
|
||||
export type COMMANDS_ALU =
|
||||
| "0"
|
||||
| "1"
|
||||
| "-1"
|
||||
| "x"
|
||||
| "y"
|
||||
| "!x"
|
||||
| "!y"
|
||||
| "-x"
|
||||
| "-y"
|
||||
| "x+1"
|
||||
| "y+1"
|
||||
| "x-1"
|
||||
| "y-1"
|
||||
| "x+y"
|
||||
| "x-y"
|
||||
| "y-x"
|
||||
| "x&y"
|
||||
| "x|y";
|
||||
|
||||
export const COMMANDS_ALU: {
|
||||
op: Record<COMMANDS_OP, COMMANDS_ALU>;
|
||||
} = {
|
||||
op: {
|
||||
0x2a: "0",
|
||||
0x3f: "1",
|
||||
0x3a: "-1",
|
||||
0x0c: "x",
|
||||
0x30: "y",
|
||||
0x0d: "!x",
|
||||
0x31: "!y",
|
||||
0x0f: "-x",
|
||||
0x33: "-y",
|
||||
0x1f: "x+1",
|
||||
0x37: "y+1",
|
||||
0x0e: "x-1",
|
||||
0x32: "y-1",
|
||||
0x02: "x+y",
|
||||
0x13: "x-y",
|
||||
0x07: "y-x",
|
||||
0x00: "x&y",
|
||||
0x15: "x|y",
|
||||
},
|
||||
};
|
||||
|
||||
export const COMMANDS: {
|
||||
asm: Record<COMMANDS_ASM, COMMANDS_OP>;
|
||||
op: Record<COMMANDS_OP, COMMANDS_ASM>;
|
||||
getOp: (asm: string) => COMMANDS_OP;
|
||||
} = {
|
||||
asm: {
|
||||
"0": 0b101010, // 42 0x2A
|
||||
"1": 0b111111, // 63 0x3F
|
||||
"-1": 0b111010, // 58 0x3A
|
||||
D: 0b001100, // 12 0x0C
|
||||
A: 0b110000, // 48 0x30
|
||||
"!D": 0b001101, // 13 0x0D
|
||||
"!A": 0b110001, // 49 0x31
|
||||
"-D": 0b001111, // 15 0x0F
|
||||
"-A": 0b110011, // 51 0x33
|
||||
"D+1": 0b011111, // 31 0x1F
|
||||
"A+1": 0b110111, // 55 0x37
|
||||
"D-1": 0b001110, // 14 0x0E
|
||||
"A-1": 0b110010, // 50 0x32
|
||||
"D+A": 0b000010, // 2 0x02
|
||||
"D-A": 0b010011, // 19 0x13
|
||||
"A-D": 0b000111, // 7 0x07
|
||||
"D&A": 0b000000, // 0 0x00
|
||||
"D|A": 0b010101, // 21 0x15
|
||||
},
|
||||
op: {
|
||||
0x2a: "0",
|
||||
0x3f: "1",
|
||||
0x3a: "-1",
|
||||
0x0c: "D",
|
||||
0x30: "A",
|
||||
0x0d: "!D",
|
||||
0x31: "!A",
|
||||
0x0f: "-D",
|
||||
0x33: "-A",
|
||||
0x1f: "D+1",
|
||||
0x37: "A+1",
|
||||
0x0e: "D-1",
|
||||
0x32: "A-1",
|
||||
0x02: "D+A",
|
||||
0x13: "D-A",
|
||||
0x07: "A-D",
|
||||
0x00: "D&A",
|
||||
0x15: "D|A",
|
||||
},
|
||||
getOp(asm: string) {
|
||||
return COMMANDS.asm[asm.replace("M", "A") as COMMANDS_ASM];
|
||||
},
|
||||
};
|
||||
|
||||
const assignAsmValues = new Set([
|
||||
"",
|
||||
"M",
|
||||
"D",
|
||||
"MD",
|
||||
"A",
|
||||
"AM",
|
||||
"AD",
|
||||
"AMD",
|
||||
] as const);
|
||||
|
||||
export type ASSIGN_ASM = typeof assignAsmValues extends Set<infer S>
|
||||
? S
|
||||
: never;
|
||||
|
||||
export type ASSIGN_OP = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
export function isAssignAsm(assign: unknown): assign is ASSIGN_ASM {
|
||||
return assignAsmValues.has(assign as ASSIGN_ASM);
|
||||
}
|
||||
|
||||
export const ASSIGN: {
|
||||
asm: Record<ASSIGN_ASM, ASSIGN_OP>;
|
||||
op: Record<ASSIGN_OP, ASSIGN_ASM>;
|
||||
} = {
|
||||
asm: {
|
||||
"": 0x0,
|
||||
M: 0b001,
|
||||
D: 0b010,
|
||||
MD: 0b011,
|
||||
A: 0b100,
|
||||
AM: 0b101,
|
||||
AD: 0b110,
|
||||
AMD: 0b111,
|
||||
},
|
||||
op: {
|
||||
0x0: "",
|
||||
0x1: "M",
|
||||
0x2: "D",
|
||||
0x3: "MD",
|
||||
0x4: "A",
|
||||
0x5: "AM",
|
||||
0x6: "AD",
|
||||
0x7: "AMD",
|
||||
},
|
||||
};
|
||||
|
||||
const jumpAsmValues = new Set([
|
||||
"",
|
||||
"JGT",
|
||||
"JEQ",
|
||||
"JGE",
|
||||
"JLT",
|
||||
"JNE",
|
||||
"JLE",
|
||||
"JMP",
|
||||
] as const);
|
||||
|
||||
export type JUMP_ASM = typeof jumpAsmValues extends Set<infer S> ? S : never;
|
||||
export type JUMP_OP = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
export function isJumpAsm(jump: unknown): jump is JUMP_ASM {
|
||||
return jumpAsmValues.has(jump as JUMP_ASM);
|
||||
}
|
||||
|
||||
export const JUMP: {
|
||||
asm: Record<JUMP_ASM, JUMP_OP>;
|
||||
op: Record<JUMP_OP, JUMP_ASM>;
|
||||
} = {
|
||||
asm: {
|
||||
"": 0b0,
|
||||
JGT: 0b001,
|
||||
JEQ: 0b010,
|
||||
JGE: 0b011,
|
||||
JLT: 0b100,
|
||||
JNE: 0b101,
|
||||
JLE: 0b110,
|
||||
JMP: 0b111,
|
||||
},
|
||||
op: {
|
||||
0x0: "",
|
||||
0x1: "JGT",
|
||||
0x2: "JEQ",
|
||||
0x3: "JGE",
|
||||
0x4: "JLT",
|
||||
0x5: "JNE",
|
||||
0x6: "JLE",
|
||||
0x7: "JMP",
|
||||
},
|
||||
};
|
||||
|
||||
export const Flags = {
|
||||
0x01: "Positive",
|
||||
0x00: "Zero",
|
||||
0x0f: "Negative",
|
||||
Positive: 0x01,
|
||||
Zero: 0x00,
|
||||
Negative: 0x0f,
|
||||
};
|
||||
|
||||
export function alu(op: number, d: number, a: number): [number, number] {
|
||||
let o = 0;
|
||||
switch (op) {
|
||||
case 0x2a:
|
||||
o = 0;
|
||||
break;
|
||||
case 0x3f:
|
||||
o = 1;
|
||||
break;
|
||||
case 0x3a:
|
||||
o = -1;
|
||||
break;
|
||||
case 0x0c:
|
||||
o = d;
|
||||
break;
|
||||
case 0x30:
|
||||
o = a;
|
||||
break;
|
||||
case 0x0d:
|
||||
o = ~d;
|
||||
break;
|
||||
case 0x31:
|
||||
o = ~a;
|
||||
break;
|
||||
case 0x0f:
|
||||
o = -d;
|
||||
break;
|
||||
case 0x33:
|
||||
o = -a;
|
||||
break;
|
||||
case 0x1f:
|
||||
o = d + 1;
|
||||
break;
|
||||
case 0x37:
|
||||
o = a + 1;
|
||||
break;
|
||||
case 0x0e:
|
||||
o = d - 1;
|
||||
break;
|
||||
case 0x32:
|
||||
o = a - 1;
|
||||
break;
|
||||
case 0x02:
|
||||
o = d + a;
|
||||
break;
|
||||
case 0x13:
|
||||
o = d - a;
|
||||
break;
|
||||
case 0x07:
|
||||
o = a - d;
|
||||
break;
|
||||
case 0x00:
|
||||
o = d & a;
|
||||
break;
|
||||
case 0x15:
|
||||
o = d | a;
|
||||
break;
|
||||
}
|
||||
|
||||
o = o & 0xffff;
|
||||
const flags =
|
||||
o === 0 ? Flags.Zero : o & 0x8000 ? Flags.Negative : Flags.Positive;
|
||||
return [o, flags];
|
||||
}
|
||||
|
||||
export function alua(op: number, d: number, a: number): [number, number] {
|
||||
if (op & 0b100000) d = 0;
|
||||
if (op & 0b010000) d = ~d & 0xffff;
|
||||
if (op & 0b001000) a = 0;
|
||||
if (op & 0b000100) a = ~a & 0xffff;
|
||||
|
||||
let o = (op & 0b000010 ? d + a : d & a) & 0xffff;
|
||||
if (op & 0b000001) o = ~o & 0xffff;
|
||||
|
||||
const flags =
|
||||
o === 0 ? Flags.Zero : o & 0x8000 ? Flags.Negative : Flags.Positive;
|
||||
return [o, flags];
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { HACK } from "../testing/mult.js";
|
||||
import { Flags } from "./alu.js";
|
||||
import { CPU, CPUInput, CPUState, cpu } from "./cpu.js";
|
||||
import { Memory } from "./memory.js";
|
||||
|
||||
describe("CPU", () => {
|
||||
describe("cpu step function", () => {
|
||||
test("@A: sets A for @ instuructions", () => {
|
||||
const input: CPUInput = { inM: 0, reset: false, instruction: 0x0002 };
|
||||
const state: CPUState = {
|
||||
A: 0,
|
||||
D: 0,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
const [output, outState] = cpu(input, state);
|
||||
|
||||
expect(output).toEqual({ outM: 0, writeM: false, addressM: 2 });
|
||||
expect(outState).toEqual({
|
||||
A: 2,
|
||||
D: 0,
|
||||
PC: 1,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
});
|
||||
});
|
||||
|
||||
test("M=1: writes to memory", () => {
|
||||
const input: CPUInput = { inM: 0, reset: false, instruction: 0xffc8 };
|
||||
const inState: CPUState = {
|
||||
A: 2,
|
||||
D: 0,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 1, writeM: true, addressM: 2 });
|
||||
expect(outState).toEqual({
|
||||
A: 2,
|
||||
D: 0,
|
||||
PC: 1,
|
||||
ALU: 1,
|
||||
flag: Flags.Positive,
|
||||
});
|
||||
});
|
||||
|
||||
test("D=M: reads from memory", () => {
|
||||
const input: CPUInput = {
|
||||
inM: 0x1234,
|
||||
reset: false,
|
||||
instruction: 0xfc10,
|
||||
};
|
||||
const inState: CPUState = { A: 0, D: 0, PC: 0, ALU: 0, flag: Flags.Zero };
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 0x1234, writeM: false, addressM: 0 });
|
||||
expect(outState).toEqual({
|
||||
A: 0,
|
||||
D: 0x1234,
|
||||
PC: 1,
|
||||
ALU: 0x1234,
|
||||
flag: Flags.Positive,
|
||||
});
|
||||
});
|
||||
|
||||
test("D;JEQ: jumps when D is 0", () => {
|
||||
const input: CPUInput = {
|
||||
inM: 0x0,
|
||||
reset: false,
|
||||
instruction: 0xd302,
|
||||
};
|
||||
const inState: CPUState = {
|
||||
A: 0xf,
|
||||
D: 0,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 0, writeM: false, addressM: 0xf });
|
||||
expect(outState).toEqual({
|
||||
A: 0xf,
|
||||
D: 0,
|
||||
PC: 15,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
});
|
||||
});
|
||||
|
||||
test("D;JEQ: does not jump when D is not 0", () => {
|
||||
const input: CPUInput = {
|
||||
inM: 0x0,
|
||||
reset: false,
|
||||
instruction: 0xd302,
|
||||
};
|
||||
const inState: CPUState = {
|
||||
A: 0xf,
|
||||
D: 3,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 3, writeM: false, addressM: 0xf });
|
||||
expect(outState).toEqual({
|
||||
A: 0xf,
|
||||
D: 3,
|
||||
PC: 1,
|
||||
ALU: 3,
|
||||
flag: Flags.Positive,
|
||||
});
|
||||
});
|
||||
|
||||
test("D=D+M: adds memory with register", () => {
|
||||
const input: CPUInput = {
|
||||
inM: 5,
|
||||
reset: false,
|
||||
instruction: 0xf090,
|
||||
};
|
||||
const inState: CPUState = { A: 0, D: 3, PC: 0, ALU: 0, flag: Flags.Zero };
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 13, writeM: false, addressM: 0 });
|
||||
expect(outState).toEqual({
|
||||
A: 0,
|
||||
D: 8,
|
||||
PC: 1,
|
||||
ALU: 13, // ALU adds at every eval
|
||||
flag: Flags.Positive,
|
||||
});
|
||||
});
|
||||
|
||||
test("@15 A=-1;JMP", () => {
|
||||
const input: CPUInput = {
|
||||
inM: 0,
|
||||
reset: false,
|
||||
instruction: 0xeea7,
|
||||
};
|
||||
const inState: CPUState = {
|
||||
A: 15,
|
||||
D: 0,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
const [output, outState] = cpu(input, inState);
|
||||
|
||||
expect(output).toEqual({ outM: 0xffff, writeM: false, addressM: 0xffff });
|
||||
expect(outState).toEqual({
|
||||
A: 0xffff,
|
||||
D: 0,
|
||||
PC: 15, // Jumped to old address
|
||||
ALU: 0xffff,
|
||||
flag: Flags.Negative,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("executes instructions", () => {
|
||||
const RAM = new Memory(256);
|
||||
RAM.set(0, 2);
|
||||
RAM.set(1, 3);
|
||||
const ROM = new Memory(HACK.buffer);
|
||||
const cpu = new CPU({ RAM, ROM });
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
cpu.tick();
|
||||
}
|
||||
|
||||
expect(RAM.get(2)).toBe(6);
|
||||
});
|
||||
|
||||
// https://github.com/nand2tetris/web-ide/issues/337
|
||||
it("MD=D+1 does not double-update on tock", () => {
|
||||
const RAM = new Memory(1);
|
||||
const ROM = new Memory(
|
||||
new Int16Array([
|
||||
0x0000, // @0
|
||||
0xefc8, // M=1 // init RAM[0]=1
|
||||
0xefd0, // D=1
|
||||
0xe7d8, // MD=D+1
|
||||
]).buffer,
|
||||
);
|
||||
|
||||
const cpu = new CPU({ RAM, ROM });
|
||||
for (let i = 0; i < 4; i++) cpu.tick();
|
||||
|
||||
expect(RAM.get(0)).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import { alu, COMMANDS_OP, Flags } from "./alu.js";
|
||||
import {
|
||||
Memory,
|
||||
MemoryAdapter,
|
||||
MemoryKeyboard,
|
||||
RAM as RAMMem,
|
||||
SCREEN_OFFSET,
|
||||
SCREEN_SIZE,
|
||||
SubMemory,
|
||||
} from "./memory.js";
|
||||
|
||||
export interface CPUInput {
|
||||
inM: number;
|
||||
instruction: number;
|
||||
reset: boolean;
|
||||
}
|
||||
|
||||
export interface CPUOutput {
|
||||
outM: number;
|
||||
writeM: boolean;
|
||||
addressM: number;
|
||||
}
|
||||
|
||||
export interface CPUState {
|
||||
A: number;
|
||||
D: number;
|
||||
PC: number;
|
||||
ALU: number;
|
||||
flag: number;
|
||||
}
|
||||
|
||||
export function emptyState(): CPUState {
|
||||
return { A: 0, D: 0, PC: 0, ALU: 0, flag: Flags.Zero };
|
||||
}
|
||||
|
||||
const BITS = {
|
||||
c: 0b1000_0000_0000_0000,
|
||||
x1: 0b1001_0000_0000_0000,
|
||||
x2: 0b1001_0000_0000_0000,
|
||||
am: 0b1001_0000_0000_0000,
|
||||
op: 0b0000_1111_1100_0000,
|
||||
d1: 0b1000_0000_0010_0000,
|
||||
d2: 0b1000_0000_0001_0000,
|
||||
d3: 0b1000_0000_0000_1000,
|
||||
j1: 0b1000_0000_0000_0001,
|
||||
j2: 0b1000_0000_0000_0010,
|
||||
j3: 0b1000_0000_0000_0100,
|
||||
};
|
||||
|
||||
export function decode(instruction: number) {
|
||||
function bit(bit: number): boolean {
|
||||
return (instruction & bit) === bit;
|
||||
}
|
||||
const bits = {
|
||||
c: bit(BITS.c),
|
||||
x1: bit(BITS.x1),
|
||||
x2: bit(BITS.x2),
|
||||
am: bit(BITS.am),
|
||||
op: ((instruction & BITS.op) >> 6) as COMMANDS_OP,
|
||||
d1: bit(BITS.d1),
|
||||
d2: bit(BITS.d2),
|
||||
d3: bit(BITS.d3),
|
||||
j1: bit(BITS.j1),
|
||||
j2: bit(BITS.j2),
|
||||
j3: bit(BITS.j3),
|
||||
};
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
export function cpuTick(
|
||||
{ inM, instruction }: CPUInput,
|
||||
{ A, D, PC }: CPUState,
|
||||
): [CPUState, boolean, number] {
|
||||
const bits = decode(instruction);
|
||||
const a = bits.am ? inM : A;
|
||||
const [ALU, flag] = alu(bits.op, D, a);
|
||||
|
||||
// While a DRegister would update during the Tock clock step,
|
||||
// this implementation updates the D internal state during tick because the test will need to access the internal D state.
|
||||
if (bits.d2) {
|
||||
D = ALU;
|
||||
}
|
||||
|
||||
return [{ A, D, PC: PC + 1, ALU, flag }, bits.d3, ALU];
|
||||
}
|
||||
|
||||
export function cpuTock(
|
||||
{ inM, instruction, reset }: CPUInput,
|
||||
{ A, D, PC, ALU, flag }: CPUState,
|
||||
): [CPUOutput, CPUState] {
|
||||
const bits = decode(instruction);
|
||||
|
||||
const j1 = bits.j1 && flag === Flags.Positive;
|
||||
const j2 = bits.j2 && flag === Flags.Zero;
|
||||
const j3 = bits.j3 && flag === Flags.Negative;
|
||||
const jmp = j1 || j2 || j3;
|
||||
|
||||
PC = reset ? 0 : jmp ? A : PC;
|
||||
|
||||
if (!bits.c) {
|
||||
A = instruction & 0x7fff;
|
||||
} else if (bits.d1) {
|
||||
A = ALU;
|
||||
}
|
||||
|
||||
const a = bits.am ? inM : A;
|
||||
const alu2 = alu(bits.op, D, a);
|
||||
|
||||
ALU = alu2[0];
|
||||
flag = alu2[1];
|
||||
|
||||
const output: CPUOutput = {
|
||||
addressM: A,
|
||||
outM: ALU,
|
||||
writeM: bits.d3,
|
||||
};
|
||||
|
||||
const state: CPUState = {
|
||||
A,
|
||||
D,
|
||||
ALU,
|
||||
flag,
|
||||
PC,
|
||||
};
|
||||
|
||||
return [output, state];
|
||||
}
|
||||
|
||||
export function cpu(input: CPUInput, state: CPUState): [CPUOutput, CPUState] {
|
||||
const [tickState, _writeM] = cpuTick(input, state);
|
||||
return cpuTock(input, tickState);
|
||||
}
|
||||
|
||||
export class CPU {
|
||||
readonly RAM: Memory;
|
||||
readonly ROM: Memory;
|
||||
readonly Screen: MemoryAdapter;
|
||||
readonly Keyboard: MemoryKeyboard;
|
||||
|
||||
#pc = 0;
|
||||
#a = 0;
|
||||
#d = 0;
|
||||
|
||||
#tickState: CPUState = {
|
||||
A: 0,
|
||||
D: 0,
|
||||
PC: 0,
|
||||
ALU: 0,
|
||||
flag: Flags.Zero,
|
||||
};
|
||||
|
||||
get state(): CPUState {
|
||||
return this.#tickState;
|
||||
}
|
||||
|
||||
get PC() {
|
||||
return this.#pc;
|
||||
}
|
||||
|
||||
get A() {
|
||||
return this.#a;
|
||||
}
|
||||
|
||||
get D() {
|
||||
return this.#d;
|
||||
}
|
||||
|
||||
setA(value: number) {
|
||||
this.#a = value;
|
||||
}
|
||||
|
||||
setD(value: number) {
|
||||
this.#d = value;
|
||||
}
|
||||
|
||||
setPC(value: number) {
|
||||
this.#pc = value;
|
||||
}
|
||||
|
||||
constructor({ RAM = new RAMMem(), ROM }: { RAM?: Memory; ROM: Memory }) {
|
||||
this.RAM = RAM;
|
||||
this.ROM = ROM;
|
||||
|
||||
// "Device Map"
|
||||
this.Screen = new SubMemory(this.RAM, SCREEN_SIZE, SCREEN_OFFSET);
|
||||
this.Keyboard = new MemoryKeyboard(this.RAM);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#pc = 0;
|
||||
this.#a = 0;
|
||||
this.#d = 0;
|
||||
}
|
||||
|
||||
tick() {
|
||||
const addressM = this.#a;
|
||||
const input = {
|
||||
inM: this.RAM.get(this.#a),
|
||||
instruction: this.ROM.get(this.#pc),
|
||||
reset: false,
|
||||
};
|
||||
|
||||
const [tickState, writeM, outM] = cpuTick(input, {
|
||||
A: this.#a,
|
||||
D: this.#d,
|
||||
PC: this.#pc,
|
||||
ALU: this.#d,
|
||||
flag: Flags.Zero,
|
||||
});
|
||||
|
||||
if (writeM) {
|
||||
this.RAM.set(addressM, outM);
|
||||
}
|
||||
|
||||
const [_, { A, D, PC }] = cpuTock(input, tickState);
|
||||
this.#a = A;
|
||||
this.#d = D;
|
||||
this.#pc = PC;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { load } from "../fs.js";
|
||||
import { op } from "../util/asm.js";
|
||||
import { int2, int10, int16 } from "../util/twos.js";
|
||||
|
||||
export const FORMATS = ["bin", "dec", "hex", "asm"];
|
||||
export type Format = (typeof FORMATS)[number];
|
||||
|
||||
export const SCREEN_OFFSET = 0x4000;
|
||||
export const SCREEN_ROWS = 256;
|
||||
export const SCREEN_COLS = 32; // These are 16-bit columns
|
||||
export const SCREEN_SIZE = SCREEN_ROWS * SCREEN_COLS;
|
||||
export const KEYBOARD_OFFSET = 0x6000;
|
||||
|
||||
export interface MemoryAdapter {
|
||||
size: number;
|
||||
get(index: number): number;
|
||||
set(index: number, value: number): void;
|
||||
reset(): void;
|
||||
update(cell: number, value: string, format: Format): void;
|
||||
load(fs: FileSystem, path: string, offset?: number): Promise<void>;
|
||||
loadBytes(bytes: number[], offset?: number): void;
|
||||
range(start?: number, end?: number): number[];
|
||||
map<T>(
|
||||
fn: (index: number, value: number) => T,
|
||||
start?: number,
|
||||
end?: number,
|
||||
): Iterable<T>;
|
||||
[Symbol.iterator](): Iterable<number>;
|
||||
}
|
||||
|
||||
export interface KeyboardAdapter {
|
||||
getKey(): number;
|
||||
setKey(key: number): void;
|
||||
clearKey(): void;
|
||||
}
|
||||
|
||||
export class Memory implements MemoryAdapter {
|
||||
private memory: Int16Array;
|
||||
|
||||
get size(): number {
|
||||
return this.memory.length;
|
||||
}
|
||||
|
||||
constructor(memory: ArrayBuffer | number) {
|
||||
if (typeof memory === "number") {
|
||||
this.memory = new Int16Array(memory);
|
||||
} else {
|
||||
this.memory = new Int16Array(memory);
|
||||
}
|
||||
}
|
||||
|
||||
get(index: number): number {
|
||||
if (index < 0 || index >= this.size) {
|
||||
return 0xffff;
|
||||
}
|
||||
return this.memory[index] ?? 0;
|
||||
}
|
||||
|
||||
set(index: number, value: number): void {
|
||||
if (index >= 0 && index < this.size) {
|
||||
this.memory[index] = value & 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.memory.fill(0);
|
||||
}
|
||||
|
||||
update(cell: number, value: string, format: Format) {
|
||||
let current: number | undefined;
|
||||
switch (format) {
|
||||
case "asm":
|
||||
try {
|
||||
current = op(value);
|
||||
} catch {
|
||||
current = undefined;
|
||||
}
|
||||
break;
|
||||
case "bin":
|
||||
current = int2(value);
|
||||
break;
|
||||
case "hex":
|
||||
current = int16(value);
|
||||
break;
|
||||
case "dec":
|
||||
default:
|
||||
current = int10(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (current !== undefined && isFinite(current) && current <= 0xffff) {
|
||||
this.set(cell, current);
|
||||
}
|
||||
}
|
||||
|
||||
async load(fs: FileSystem, path: string, offset?: number) {
|
||||
try {
|
||||
this.loadBytes(await load(fs, path), offset);
|
||||
} catch (_cause) {
|
||||
// throw new Error(`ROM32K Failed to load file ${path}`, { cause });
|
||||
throw new Error(`Memory Failed to load file ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
loadBytes(bytes: number[], offset?: number): void {
|
||||
this.memory.set(new Int16Array(bytes), offset);
|
||||
this.memory.fill(0, bytes.length, this.size);
|
||||
}
|
||||
|
||||
range(start = 0, end = this.size): number[] {
|
||||
return [...this.memory.slice(start, end)];
|
||||
}
|
||||
|
||||
*map<T>(
|
||||
fn: (index: number, value: number) => T,
|
||||
start = 0,
|
||||
end = this.size,
|
||||
): Iterable<T> {
|
||||
assert(start <= end);
|
||||
for (let i = start; i < end; i++) {
|
||||
yield fn(i, this.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterable<number> {
|
||||
return this.map((_, v) => v);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.memory.every((word) => word === 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class SubMemory implements MemoryAdapter {
|
||||
constructor(
|
||||
private readonly parent: MemoryAdapter,
|
||||
readonly size: number,
|
||||
private readonly offset: number,
|
||||
) {}
|
||||
|
||||
get(index: number): number {
|
||||
if (index < 0 || index >= this.size) {
|
||||
return 0xffff;
|
||||
}
|
||||
return this.parent.get(this.offset + index);
|
||||
}
|
||||
|
||||
set(index: number, value: number, trackChange = true): void {
|
||||
if (index >= 0 && index < this.size) {
|
||||
this.parent.set(index + this.offset, value);
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
this.set(i, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
update(index: number, value: string, format: string): void {
|
||||
if (index >= 0 && index < this.size) {
|
||||
this.parent.update(index + this.offset, value, format);
|
||||
}
|
||||
}
|
||||
|
||||
load(fs: FileSystem, path: string): Promise<void> {
|
||||
return this.parent.load(fs, path, this.offset);
|
||||
}
|
||||
|
||||
loadBytes(bytes: number[]): void {
|
||||
return this.parent.loadBytes(bytes, this.offset);
|
||||
}
|
||||
|
||||
range(start?: number, end?: number): number[] {
|
||||
return this.parent.range(start, end);
|
||||
}
|
||||
|
||||
map<T>(
|
||||
fn: (index: number, value: number) => T,
|
||||
start = 0,
|
||||
end: number = this.size,
|
||||
): Iterable<T> {
|
||||
return this.parent.map(fn, start + this.offset, end + this.offset);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterable<number> {
|
||||
return this.map((_, v) => v);
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryKeyboard extends SubMemory implements KeyboardAdapter {
|
||||
constructor(memory: MemoryAdapter) {
|
||||
super(memory, 1, 0x6000);
|
||||
}
|
||||
|
||||
getKey(): number {
|
||||
return this.get(0);
|
||||
}
|
||||
|
||||
setKey(key: number): void {
|
||||
this.set(0, key & 0xffff);
|
||||
}
|
||||
|
||||
clearKey(): void {
|
||||
this.set(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class ROM extends Memory {
|
||||
static readonly SIZE = 0x8000;
|
||||
constructor(program?: Int16Array) {
|
||||
if (program) {
|
||||
const arr = new Int16Array(ROM.SIZE);
|
||||
arr.set(program);
|
||||
super(arr.buffer);
|
||||
} else {
|
||||
super(ROM.SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM extends Memory {
|
||||
keyboard = new SubMemory(this, 1, KEYBOARD_OFFSET);
|
||||
screen = new SubMemory(this, SCREEN_SIZE, SCREEN_OFFSET);
|
||||
|
||||
// 4k main memory, 2k screen memory, 1 keyboard
|
||||
static readonly SIZE = 0x4000 + 0x2000 + 0x0001;
|
||||
|
||||
constructor() {
|
||||
super(RAM.SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import * as loader from "./loader.js";
|
||||
|
||||
export async function load(fs: FileSystem, path: string): Promise<number[]> {
|
||||
if (path.endsWith(".hack")) {
|
||||
return loadHack(fs, path);
|
||||
}
|
||||
|
||||
if (path.endsWith(".asm")) {
|
||||
return loadAsm(fs, path);
|
||||
}
|
||||
|
||||
throw new Error(`Cannot load file without hack or asm extension ${path}`);
|
||||
}
|
||||
|
||||
export async function loadAsm(fs: FileSystem, path: string): Promise<number[]> {
|
||||
return loader.loadAsm(await fs.readFile(path));
|
||||
}
|
||||
|
||||
export async function loadHack(
|
||||
fs: FileSystem,
|
||||
path: string,
|
||||
): Promise<number[]> {
|
||||
return loader.loadHack(await fs.readFile(path));
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Programs } from "@nand2tetris/projects/samples/project_11/index.js";
|
||||
import { JACK } from "../languages/jack";
|
||||
import { Compiler, compile } from "./compiler";
|
||||
|
||||
function parse(code: string, rule: string) {
|
||||
return JACK.semantics(JACK.parser.match(code, rule));
|
||||
}
|
||||
|
||||
describe("compiler", () => {
|
||||
it("compiles expression", () => {
|
||||
const exp = parse("(2 + 3) * 5", "Expression").expression;
|
||||
|
||||
const compiler = new Compiler();
|
||||
compiler.compileExpression(exp);
|
||||
expect(compiler.output).toEqual([
|
||||
"push constant 2",
|
||||
"push constant 3",
|
||||
"add",
|
||||
"push constant 5",
|
||||
"call Math.multiply 2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compiles function", () => {
|
||||
const func = parse(
|
||||
`function void main() {
|
||||
var int a;
|
||||
let a = 4;
|
||||
return;
|
||||
}`,
|
||||
"SubroutineDec",
|
||||
).subroutineDec;
|
||||
|
||||
const compiler = new Compiler();
|
||||
compiler.className = "Main";
|
||||
compiler.compileFunction(func);
|
||||
expect(compiler.output).toEqual([
|
||||
"function Main.main 1",
|
||||
"push constant 4",
|
||||
"pop local 0",
|
||||
"push constant 0",
|
||||
"return",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compiles array access", () => {
|
||||
const statement = parse(`let x = arr[2];`, "Statement").statement;
|
||||
|
||||
const compiler = new Compiler();
|
||||
compiler.localSymbolTable = {
|
||||
x: {
|
||||
type: "int",
|
||||
segment: "local",
|
||||
index: 0,
|
||||
},
|
||||
arr: {
|
||||
type: "Array",
|
||||
segment: "local",
|
||||
index: 1,
|
||||
},
|
||||
};
|
||||
compiler.compileStatement(statement);
|
||||
expect(compiler.output).toEqual([
|
||||
"push constant 2",
|
||||
"push local 1",
|
||||
"add",
|
||||
"pop pointer 1",
|
||||
"push that 0",
|
||||
"pop local 0",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compiles if-else", () => {
|
||||
const statement = parse(
|
||||
`if (condition) {
|
||||
let x = 4;
|
||||
} else {
|
||||
let x = 5;
|
||||
}`,
|
||||
"Statement",
|
||||
).statement;
|
||||
|
||||
const compiler = new Compiler();
|
||||
compiler.className = "Main";
|
||||
compiler.localSymbolTable = {
|
||||
condition: {
|
||||
type: "boolean",
|
||||
segment: "local",
|
||||
index: 0,
|
||||
},
|
||||
x: {
|
||||
type: "int",
|
||||
segment: "local",
|
||||
index: 1,
|
||||
},
|
||||
};
|
||||
compiler.compileStatement(statement);
|
||||
expect(compiler.output).toEqual([
|
||||
"push local 0",
|
||||
"not",
|
||||
"if-goto Main_1",
|
||||
"push constant 4",
|
||||
"pop local 1",
|
||||
"goto Main_0",
|
||||
"label Main_1",
|
||||
"push constant 5",
|
||||
"pop local 1",
|
||||
"label Main_0",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(Object.keys(Programs))("%s", (program) => {
|
||||
const compiled = compile(
|
||||
Object.fromEntries(
|
||||
Object.entries(Programs[program]).map(([name, file]) => [
|
||||
name,
|
||||
file.jack,
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
for (const file of Object.keys(compiled)) {
|
||||
expect(compiled[file]).toEqual(Programs[program][file].compiled);
|
||||
}
|
||||
});
|
||||
|
||||
it("compiles a class with no fields", () => {
|
||||
const compiled = compile({
|
||||
NoField: `
|
||||
class NoField {
|
||||
constructor NoField new() {
|
||||
return this;
|
||||
}
|
||||
method void dispose() {
|
||||
do Memory.deAlloc(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
`,
|
||||
Main: `
|
||||
class Main {
|
||||
function void main() {
|
||||
var NoField z;
|
||||
let z = NoField.new();
|
||||
do z.dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const noFieldVm = compiled["NoField"];
|
||||
expect(noFieldVm).toContain("function NoField.new 0");
|
||||
expect(noFieldVm).not.toContain("call Memory.alloc 1");
|
||||
expect(noFieldVm).toContain("function NoField.dispose 0");
|
||||
expect(noFieldVm).not.toContain("call Memory.deAlloc 1");
|
||||
|
||||
const mainVm = compiled["Main"];
|
||||
expect(mainVm).toContain("function Main.main 1");
|
||||
expect(mainVm).toContain("call NoField.new 0");
|
||||
expect(mainVm).toContain("call NoField.dispose 1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,651 @@
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CompilationError, createError, Span } from "../languages/base.js";
|
||||
import {
|
||||
ArrayAccess,
|
||||
Class,
|
||||
ClassVarDec,
|
||||
DoStatement,
|
||||
Expression,
|
||||
IfStatement,
|
||||
isPrimitive,
|
||||
JACK,
|
||||
KeywordConstant,
|
||||
LetStatement,
|
||||
Op,
|
||||
Parameter,
|
||||
ReturnStatement,
|
||||
Statement,
|
||||
Subroutine,
|
||||
SubroutineCall,
|
||||
Term,
|
||||
Type,
|
||||
UnaryOp,
|
||||
VarDec,
|
||||
Variable,
|
||||
WhileStatement,
|
||||
} from "../languages/jack.js";
|
||||
import { Segment } from "../languages/vm.js";
|
||||
import {
|
||||
makeInterface,
|
||||
overridesOsCorrectly,
|
||||
VM_BUILTINS,
|
||||
} from "../vm/builtins.js";
|
||||
import { validateSubroutine } from "./controlFlow.js";
|
||||
|
||||
const osClasses = new Set([
|
||||
"Sys",
|
||||
"Screen",
|
||||
"Output",
|
||||
"Keyboard",
|
||||
"String",
|
||||
"Array",
|
||||
"Memory",
|
||||
"Math",
|
||||
]);
|
||||
|
||||
function isOsClass(name: string): boolean {
|
||||
return osClasses.has(name);
|
||||
}
|
||||
|
||||
function isError(value: unknown): value is CompilationError {
|
||||
return (value as CompilationError).message != undefined;
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
export function compile(
|
||||
files: Record<string, string>,
|
||||
): Record<string, string | CompilationError> {
|
||||
const classes: Record<string, Class | CompilationError> = {};
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
const parsed = JACK.parse(content);
|
||||
if (isErr(parsed)) {
|
||||
classes[name] = Err(parsed);
|
||||
} else {
|
||||
const cls = Ok(parsed);
|
||||
const result = validateClass(cls);
|
||||
classes[name] =
|
||||
cls.name.value == name
|
||||
? isErr(result)
|
||||
? Err(result)
|
||||
: cls
|
||||
: createError(
|
||||
`Class name ${cls.name.value} doesn't match file name ${name}`,
|
||||
cls.name.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const validClasses: Record<string, Class> = Object.fromEntries(
|
||||
Object.entries(classes).filter(([_, parsed]) => !isError(parsed)),
|
||||
) as Record<string, Class>;
|
||||
|
||||
const vms: Record<string, string | CompilationError> = {};
|
||||
for (const [name, parsed] of Object.entries(classes)) {
|
||||
if (isError(parsed)) {
|
||||
vms[name] = parsed;
|
||||
} else {
|
||||
try {
|
||||
const compiled = new Compiler().compile(parsed, validClasses);
|
||||
if (isErr(compiled)) {
|
||||
vms[name] = Err(compiled);
|
||||
} else {
|
||||
vms[name] = Ok(compiled);
|
||||
}
|
||||
} catch (e) {
|
||||
vms[name] = e as CompilationError;
|
||||
}
|
||||
}
|
||||
}
|
||||
return vms;
|
||||
}
|
||||
|
||||
function validateClass(cls: Class): Result<void, CompilationError> {
|
||||
const subroutineNames = new Set<string>();
|
||||
for (const subroutine of cls.subroutines) {
|
||||
if (subroutineNames.has(subroutine.name.value)) {
|
||||
return Err(
|
||||
createError(
|
||||
`Subroutine ${subroutine.name.value} already declared`,
|
||||
subroutine.name.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
subroutineNames.add(subroutine.name.value);
|
||||
const result = validateSubroutine(subroutine);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
interface VariableData {
|
||||
type: Type;
|
||||
segment: Segment;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const ops: Record<Op, string> = {
|
||||
"+": "add",
|
||||
"-": "sub",
|
||||
"*": "call Math.multiply 2",
|
||||
"/": "call Math.divide 2",
|
||||
"&": "and",
|
||||
"|": "or",
|
||||
"<": "lt",
|
||||
">": "gt",
|
||||
"=": "eq",
|
||||
};
|
||||
|
||||
const unaryOps: Record<UnaryOp, string> = {
|
||||
"-": "neg",
|
||||
"~": "not",
|
||||
};
|
||||
|
||||
interface SubroutineCallAttributes {
|
||||
className: string;
|
||||
subroutineName: string;
|
||||
object?: string; // object being acted upon if this is a method (undefined if function / constructor)
|
||||
}
|
||||
|
||||
export class Compiler {
|
||||
private instructions: string[] = [];
|
||||
globalSymbolTable: Record<string, VariableData> = {};
|
||||
localSymbolTable: Record<string, VariableData> = {};
|
||||
|
||||
className = "";
|
||||
private classes: Record<string, Class> = {};
|
||||
|
||||
private labelNum = 0;
|
||||
private fieldNum = 0;
|
||||
private staticNum = 0;
|
||||
private localNum = 0;
|
||||
|
||||
get output(): string[] {
|
||||
return Array.from(this.instructions);
|
||||
}
|
||||
|
||||
varData(name: string): VariableData | undefined {
|
||||
return this.localSymbolTable[name] || this.globalSymbolTable[name];
|
||||
}
|
||||
|
||||
var(name: string): string;
|
||||
var(variable: Variable): string;
|
||||
var(variable: ArrayAccess): string;
|
||||
var(variable: LetStatement): string;
|
||||
var(arg: string | Variable | ArrayAccess | LetStatement): string {
|
||||
let name: string;
|
||||
let span: Span | undefined;
|
||||
if (typeof arg == "string") {
|
||||
name = arg;
|
||||
} else {
|
||||
if (typeof arg.name == "string") {
|
||||
name = arg.name;
|
||||
span = arg.span;
|
||||
} else {
|
||||
name = arg.name.value;
|
||||
span = arg.name.span;
|
||||
}
|
||||
}
|
||||
const data = this.varData(name);
|
||||
if (!data) {
|
||||
throw createError(`Undeclared variable ${name}`, span);
|
||||
}
|
||||
return `${data.segment} ${data.index}`;
|
||||
}
|
||||
|
||||
write(...lines: string[]) {
|
||||
this.instructions.push(...lines);
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
const label = `${this.className}_${this.labelNum}`;
|
||||
this.labelNum += 1;
|
||||
return label;
|
||||
}
|
||||
|
||||
compile(
|
||||
cls: Class,
|
||||
other?: Record<string, Class>,
|
||||
): Result<string, CompilationError> {
|
||||
this.className = cls.name.value;
|
||||
this.classes = other ?? {};
|
||||
for (const varDec of cls.varDecs) {
|
||||
this.compileClassVarDec(varDec);
|
||||
}
|
||||
for (const subroutine of cls.subroutines) {
|
||||
this.compileSubroutineDec(subroutine);
|
||||
}
|
||||
return Ok(
|
||||
this.instructions
|
||||
.map((inst) =>
|
||||
inst.startsWith("function") || inst.startsWith("label")
|
||||
? inst
|
||||
: " ".concat(inst),
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
validateType(type: string, span?: Span) {
|
||||
if (isPrimitive(type) || isOsClass(type) || this.classes[type]) {
|
||||
return;
|
||||
}
|
||||
throw createError(`Unknown type ${type}`, span);
|
||||
}
|
||||
|
||||
validateReturnType(returnType: string, span?: Span) {
|
||||
if (returnType == "void") {
|
||||
return;
|
||||
}
|
||||
this.validateType(returnType, span);
|
||||
}
|
||||
|
||||
compileClassVarDec(dec: ClassVarDec) {
|
||||
this.validateType(dec.type.value, dec.type.span);
|
||||
for (const name of dec.names) {
|
||||
if (dec.varType == "field") {
|
||||
this.globalSymbolTable[name] = {
|
||||
type: dec.type.value,
|
||||
segment: "this",
|
||||
index: this.fieldNum,
|
||||
};
|
||||
this.fieldNum += 1;
|
||||
} else {
|
||||
this.globalSymbolTable[name] = {
|
||||
type: dec.type.value,
|
||||
segment: "static",
|
||||
index: this.staticNum,
|
||||
};
|
||||
this.staticNum += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileVarDec(dec: VarDec) {
|
||||
this.validateType(dec.type.value, dec.type.span);
|
||||
for (const name of dec.names) {
|
||||
this.localSymbolTable[name] = {
|
||||
type: dec.type.value,
|
||||
segment: "local",
|
||||
index: this.localNum,
|
||||
};
|
||||
this.localNum += 1;
|
||||
}
|
||||
}
|
||||
|
||||
registerArgs(params: Parameter[], offset = false) {
|
||||
let argNum = 0;
|
||||
for (const param of params) {
|
||||
this.validateType(param.type.value, param.type.span);
|
||||
this.localSymbolTable[param.name] = {
|
||||
type: param.type.value,
|
||||
segment: "argument",
|
||||
index: argNum + (offset ? 1 : 0), // when compiling a method the first argument is this, so we offset the others by 1
|
||||
};
|
||||
argNum += 1;
|
||||
}
|
||||
}
|
||||
|
||||
validateSubroutineDec(subroutine: Subroutine) {
|
||||
this.validateReturnType(
|
||||
subroutine.returnType.value,
|
||||
subroutine.returnType.span,
|
||||
);
|
||||
|
||||
if (isOsClass(this.className)) {
|
||||
const builtin = VM_BUILTINS[`${this.className}.${subroutine.name.value}`];
|
||||
|
||||
if (builtin && !overridesOsCorrectly(this.className, subroutine)) {
|
||||
throw createError(
|
||||
`OS subroutine ${this.className}.${subroutine.name.value} must follow the interface ${makeInterface(subroutine.name.value, builtin)})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileSubroutineDec(subroutine: Subroutine) {
|
||||
this.validateSubroutineDec(subroutine);
|
||||
switch (subroutine.type) {
|
||||
case "method":
|
||||
this.compileMethod(subroutine);
|
||||
break;
|
||||
case "constructor":
|
||||
this.compileConstructor(subroutine);
|
||||
break;
|
||||
case "function":
|
||||
this.compileFunction(subroutine);
|
||||
}
|
||||
}
|
||||
|
||||
compileSubroutineStart(subroutine: Subroutine, isMethod = false) {
|
||||
this.localSymbolTable = {};
|
||||
this.localNum = 0;
|
||||
this.registerArgs(subroutine.parameters, isMethod);
|
||||
|
||||
const localCount = subroutine.body.varDecs
|
||||
.map((dec) => dec.names.length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
this.write(
|
||||
`function ${this.className}.${subroutine.name.value} ${localCount}`,
|
||||
);
|
||||
for (const varDec of subroutine.body.varDecs) {
|
||||
this.compileVarDec(varDec);
|
||||
}
|
||||
}
|
||||
|
||||
compileFunction(subroutine: Subroutine) {
|
||||
this.compileSubroutineStart(subroutine);
|
||||
this.compileStatements(subroutine.body.statements);
|
||||
}
|
||||
|
||||
compileMethod(subroutine: Subroutine) {
|
||||
this.compileSubroutineStart(subroutine, true);
|
||||
this.write("push argument 0", "pop pointer 0");
|
||||
this.compileStatements(subroutine.body.statements);
|
||||
}
|
||||
|
||||
compileConstructor(subroutine: Subroutine) {
|
||||
this.compileSubroutineStart(subroutine);
|
||||
|
||||
if (this.fieldNum > 0)
|
||||
this.write(
|
||||
`push constant ${this.fieldNum}`,
|
||||
"call Memory.alloc 1",
|
||||
"pop pointer 0",
|
||||
);
|
||||
else this.write("push constant 0", "pop pointer 0");
|
||||
|
||||
this.compileStatements(subroutine.body.statements);
|
||||
}
|
||||
|
||||
compileExpression(expression: Expression) {
|
||||
this.compileTerm(expression.term);
|
||||
for (const part of expression.rest) {
|
||||
this.compileTerm(part.term);
|
||||
this.compileOp(part.op); // postfix
|
||||
}
|
||||
}
|
||||
|
||||
compileOp(op: Op) {
|
||||
this.write(ops[op]);
|
||||
}
|
||||
|
||||
compileTerm(term: Term) {
|
||||
switch (term.termType) {
|
||||
case "numericLiteral":
|
||||
this.write(`push constant ${term.value}`);
|
||||
break;
|
||||
case "stringLiteral":
|
||||
this.compileStringLiteral(term.value);
|
||||
break;
|
||||
case "variable":
|
||||
this.write(`push ${this.var(term)}`);
|
||||
break;
|
||||
case "keywordLiteral":
|
||||
this.compileKeywordLiteral(term.value);
|
||||
break;
|
||||
case "subroutineCall":
|
||||
this.compileSubroutineCall(term);
|
||||
break;
|
||||
case "arrayAccess":
|
||||
this.compileExpression(term.index);
|
||||
this.write(
|
||||
`push ${this.var(term)}`,
|
||||
"add",
|
||||
"pop pointer 1",
|
||||
"push that 0",
|
||||
);
|
||||
break;
|
||||
case "groupedExpression":
|
||||
this.compileExpression(term.expression);
|
||||
break;
|
||||
case "unaryExpression":
|
||||
this.compileTerm(term.term);
|
||||
this.write(unaryOps[term.op]);
|
||||
}
|
||||
}
|
||||
|
||||
validateArgNum(name: string, expected: number, call: SubroutineCall) {
|
||||
const received = call.parameters.length;
|
||||
if (expected != received) {
|
||||
throw createError(
|
||||
`${name} expected ${expected} arguments, got ${received}`,
|
||||
call.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateSubroutineCall(
|
||||
className: string,
|
||||
subroutineName: string,
|
||||
call: SubroutineCall,
|
||||
isMethod: boolean,
|
||||
) {
|
||||
const builtin = VM_BUILTINS[`${className}.${subroutineName}`];
|
||||
if (builtin) {
|
||||
if (builtin.type == "method" && !isMethod) {
|
||||
throw createError(
|
||||
`Method ${className}.${subroutineName} was called as a function/constructor`,
|
||||
call.name.span,
|
||||
);
|
||||
}
|
||||
if (builtin.type != "method" && isMethod) {
|
||||
throw createError(
|
||||
`${capitalize(
|
||||
builtin.type,
|
||||
)} ${className}.${subroutineName} was called as a method`,
|
||||
call.name.span,
|
||||
);
|
||||
}
|
||||
this.validateArgNum(
|
||||
`${className}.${subroutineName}`,
|
||||
builtin.args.length,
|
||||
call,
|
||||
);
|
||||
return;
|
||||
} else if (this.classes[className]) {
|
||||
for (const subroutine of this.classes[className].subroutines) {
|
||||
if (subroutine.name.value == subroutineName) {
|
||||
if (subroutine.type == "method" && !isMethod) {
|
||||
throw createError(
|
||||
`Method ${className}.${subroutineName} was called as a function/constructor`,
|
||||
call.name.span,
|
||||
);
|
||||
}
|
||||
if (subroutine.type != "method" && isMethod) {
|
||||
throw createError(
|
||||
`${capitalize(
|
||||
subroutine.name.value,
|
||||
)} ${className}.${subroutineName} was called as a method`,
|
||||
call.name.span,
|
||||
);
|
||||
}
|
||||
this.validateArgNum(
|
||||
`${className}.${subroutineName}`,
|
||||
subroutine.parameters.length,
|
||||
call,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw createError(
|
||||
`Class ${className} doesn't contain a function/constructor ${subroutineName}`,
|
||||
call.name.span,
|
||||
);
|
||||
} else {
|
||||
throw createError(`Class ${className} doesn't exist`, call.name.span);
|
||||
}
|
||||
}
|
||||
|
||||
classifySubroutineCall(call: SubroutineCall): SubroutineCallAttributes {
|
||||
let object: string | undefined;
|
||||
let className = "";
|
||||
let subroutineName = "";
|
||||
|
||||
if (call.name.value.includes(".")) {
|
||||
const [prefix, suffix] = call.name.value.split(".", 2);
|
||||
subroutineName = suffix;
|
||||
const varData = this.varData(prefix);
|
||||
if (varData) {
|
||||
// external method call
|
||||
object = this.var(prefix);
|
||||
className = varData.type;
|
||||
} else {
|
||||
// function / constructor call
|
||||
className = prefix;
|
||||
}
|
||||
} else {
|
||||
object = "pointer 0"; // this
|
||||
className = this.className;
|
||||
subroutineName = call.name.value;
|
||||
}
|
||||
|
||||
this.validateSubroutineCall(
|
||||
className,
|
||||
subroutineName,
|
||||
call,
|
||||
object != undefined,
|
||||
);
|
||||
|
||||
return { className, subroutineName, object };
|
||||
}
|
||||
|
||||
compileSubroutineCall(call: SubroutineCall) {
|
||||
const attributes = this.classifySubroutineCall(call);
|
||||
|
||||
if (
|
||||
attributes.className === "Memory" &&
|
||||
attributes.subroutineName === "deAlloc" &&
|
||||
this.fieldNum === 0
|
||||
) {
|
||||
for (const param of call.parameters) {
|
||||
this.compileExpression(param);
|
||||
this.write("pop temp 0");
|
||||
}
|
||||
|
||||
this.write("push constant 0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributes.object) {
|
||||
this.write(`push ${attributes.object}`);
|
||||
}
|
||||
for (const param of call.parameters) {
|
||||
this.compileExpression(param);
|
||||
}
|
||||
this.write(
|
||||
`call ${attributes.className}.${attributes.subroutineName} ${
|
||||
call.parameters.length + (attributes.object ? 1 : 0)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
compileStringLiteral(str: string) {
|
||||
this.write(`push constant ${str.length}`, `call String.new 1`);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
this.write(
|
||||
`push constant ${str.charCodeAt(i)}`,
|
||||
`call String.appendChar 2`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
compileKeywordLiteral(keyword: KeywordConstant) {
|
||||
switch (keyword) {
|
||||
case "true":
|
||||
this.write(`push constant 1`, `neg`);
|
||||
break;
|
||||
case "false":
|
||||
this.write(`push constant 0`);
|
||||
break;
|
||||
case "null":
|
||||
this.write(`push constant 0`);
|
||||
break;
|
||||
case "this":
|
||||
this.write(`push pointer 0`);
|
||||
}
|
||||
}
|
||||
|
||||
compileStatements(statements: Statement[]) {
|
||||
for (const statement of statements) {
|
||||
this.compileStatement(statement);
|
||||
}
|
||||
}
|
||||
|
||||
compileStatement(statement: Statement) {
|
||||
switch (statement.statementType) {
|
||||
case "doStatement":
|
||||
this.compileDoStatement(statement);
|
||||
break;
|
||||
case "ifStatement":
|
||||
this.compileIf(statement);
|
||||
break;
|
||||
case "letStatement":
|
||||
this.compileLet(statement);
|
||||
break;
|
||||
case "returnStatement":
|
||||
this.compileReturn(statement);
|
||||
break;
|
||||
case "whileStatement":
|
||||
this.compileWhile(statement);
|
||||
}
|
||||
}
|
||||
|
||||
compileReturn(statement: ReturnStatement) {
|
||||
if (statement.value) {
|
||||
this.compileExpression(statement.value);
|
||||
} else {
|
||||
this.write(`push constant 0`); // return 0
|
||||
}
|
||||
this.write(`return`);
|
||||
}
|
||||
|
||||
compileLet(statement: LetStatement) {
|
||||
if (statement.arrayIndex) {
|
||||
this.compileExpression(statement.arrayIndex);
|
||||
this.write(`push ${this.var(statement)}`, "add");
|
||||
this.compileExpression(statement.value);
|
||||
this.write("pop temp 0", "pop pointer 1", "push temp 0", "pop that 0");
|
||||
} else {
|
||||
this.compileExpression(statement.value);
|
||||
this.write(`pop ${this.var(statement)}`);
|
||||
}
|
||||
}
|
||||
|
||||
compileDoStatement(statement: DoStatement) {
|
||||
this.compileSubroutineCall(statement.call);
|
||||
this.write(`pop temp 0`);
|
||||
}
|
||||
|
||||
compileIf(statement: IfStatement) {
|
||||
const condTrue = this.getLabel();
|
||||
const condFalse = this.getLabel();
|
||||
|
||||
this.compileExpression(statement.condition);
|
||||
this.write("not", `if-goto ${condFalse}`);
|
||||
this.compileStatements(statement.body);
|
||||
this.write(`goto ${condTrue}`, `label ${condFalse}`);
|
||||
this.compileStatements(statement.else);
|
||||
this.write(`label ${condTrue}`);
|
||||
}
|
||||
|
||||
compileWhile(statement: WhileStatement) {
|
||||
const loop = this.getLabel();
|
||||
const exit = this.getLabel();
|
||||
|
||||
this.write(`label ${loop}`);
|
||||
this.compileExpression(statement.condition);
|
||||
this.write(`not`, `if-goto ${exit}`);
|
||||
this.compileStatements(statement.body);
|
||||
this.write(`goto ${loop}`, `label ${exit}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
unwrap,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CompilationError, createError } from "../languages/base.js";
|
||||
import {
|
||||
IfStatement,
|
||||
ReturnType,
|
||||
Statement,
|
||||
Subroutine,
|
||||
WhileStatement,
|
||||
} from "../languages/jack.js";
|
||||
|
||||
class CFGNode {
|
||||
id: number;
|
||||
hasReturn = false;
|
||||
children: CFGNode[] = [];
|
||||
|
||||
static count = 0;
|
||||
|
||||
constructor() {
|
||||
this.id = CFGNode.count;
|
||||
CFGNode.count += 1;
|
||||
}
|
||||
|
||||
alwaysReturns(): boolean {
|
||||
const visited: Set<CFGNode> = new Set();
|
||||
|
||||
function checkReturn(node: CFGNode): boolean {
|
||||
if (node.hasReturn) {
|
||||
return true;
|
||||
} else if (node.children.length === 0) {
|
||||
return false;
|
||||
}
|
||||
visited.add(node);
|
||||
for (const child of node.children) {
|
||||
if (!visited.has(child) && !checkReturn(child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkReturn(this);
|
||||
}
|
||||
|
||||
getLeafs(): CFGNode[] {
|
||||
const leafs: Set<CFGNode> = new Set();
|
||||
const visited: Set<CFGNode> = new Set();
|
||||
|
||||
function findLeafs(node: CFGNode) {
|
||||
if (node.children.length === 0) {
|
||||
leafs.add(node);
|
||||
} else {
|
||||
visited.add(node);
|
||||
for (const child of node.children) {
|
||||
if (!visited.has(child)) {
|
||||
findLeafs(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findLeafs(this);
|
||||
return Array.from(leafs);
|
||||
}
|
||||
}
|
||||
|
||||
function processIf(
|
||||
statement: IfStatement,
|
||||
returnType: ReturnType,
|
||||
current: CFGNode,
|
||||
): Result<CFGNode, CompilationError> {
|
||||
const ifStart = new CFGNode();
|
||||
current.children.push(ifStart);
|
||||
current = ifStart;
|
||||
const result1 = buildCFG(statement.body, returnType);
|
||||
const result2 = buildCFG(statement.else, returnType);
|
||||
if (isErr(result1)) {
|
||||
return result1;
|
||||
}
|
||||
if (isErr(result2)) {
|
||||
return result2;
|
||||
}
|
||||
const path1 = unwrap(result1);
|
||||
const path2 = unwrap(result2);
|
||||
current.children.push(path1, path2);
|
||||
const leafs = path1.getLeafs().concat(path2.getLeafs());
|
||||
current = new CFGNode();
|
||||
for (const leaf of leafs) {
|
||||
leaf.children.push(current);
|
||||
}
|
||||
return Ok(current);
|
||||
}
|
||||
|
||||
function processWhile(
|
||||
statement: WhileStatement,
|
||||
returnType: ReturnType,
|
||||
current: CFGNode,
|
||||
): Result<CFGNode, CompilationError> {
|
||||
const whileStart = new CFGNode();
|
||||
current.children.push(whileStart);
|
||||
current = whileStart;
|
||||
const result = buildCFG(statement.body, returnType);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
const body = unwrap(result);
|
||||
for (const leaf of body.getLeafs()) {
|
||||
leaf.children.push(current);
|
||||
}
|
||||
const next = new CFGNode();
|
||||
current.children.push(body, next);
|
||||
current = next;
|
||||
return Ok(current);
|
||||
}
|
||||
|
||||
function buildCFG(
|
||||
statements: Statement[],
|
||||
returnType: ReturnType,
|
||||
): Result<CFGNode, CompilationError> {
|
||||
const root = new CFGNode();
|
||||
let current = root;
|
||||
|
||||
let result: Result<CFGNode, CompilationError> | undefined;
|
||||
for (const statement of statements) {
|
||||
switch (statement.statementType) {
|
||||
case "letStatement":
|
||||
case "doStatement":
|
||||
break;
|
||||
case "returnStatement":
|
||||
if (returnType != "void" && statement.value == undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
`A non void subroutine must return a value`,
|
||||
statement.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
current.hasReturn = true;
|
||||
break;
|
||||
case "ifStatement":
|
||||
result = processIf(statement, returnType, current);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
current = unwrap(result);
|
||||
break;
|
||||
case "whileStatement":
|
||||
result = processWhile(statement, returnType, current);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
current = unwrap(result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(root);
|
||||
}
|
||||
|
||||
export function validateSubroutine(
|
||||
subroutine: Subroutine,
|
||||
): Result<void, CompilationError> {
|
||||
const cfg = buildCFG(subroutine.body.statements, subroutine.returnType.value);
|
||||
if (isErr(cfg)) {
|
||||
return cfg;
|
||||
}
|
||||
if (!unwrap(cfg).alwaysReturns()) {
|
||||
return Err(
|
||||
createError(
|
||||
`Subroutine ${subroutine.name.value}: not all code paths return a value`,
|
||||
subroutine.name.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import { MaxAsm } from "@nand2tetris/projects/samples/project_06/02_max.js";
|
||||
import { ASSIGN, COMMANDS, JUMP } from "../cpu/alu.js";
|
||||
import { Asm, asmSemantics, emit, fillLabel, grammar } from "./asm.js";
|
||||
|
||||
describe("asm language", () => {
|
||||
it("parses an empty file", () => {
|
||||
const match = grammar.match("");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).asm).toEqual({ instructions: [] });
|
||||
});
|
||||
|
||||
it("parses an A instruction to a label", () => {
|
||||
const match = grammar.match("@R0", "aInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "A",
|
||||
label: "R0",
|
||||
value: undefined,
|
||||
span: { line: 1, start: 0, end: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an A instruction to a value", () => {
|
||||
const match = grammar.match("@5", "aInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "A",
|
||||
label: undefined,
|
||||
value: 5,
|
||||
span: { line: 1, start: 0, end: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a C instruction", () => {
|
||||
const match = grammar.match("-1", "cInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("-1"),
|
||||
isM: false,
|
||||
span: { line: 1, start: 0, end: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a C instruction with assignment", () => {
|
||||
const match = grammar.match("D=M", "cInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 1, start: 0, end: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a C instruction with operation", () => {
|
||||
const match = grammar.match("M=M+1", "cInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("A+1"),
|
||||
store: ASSIGN.asm["M"],
|
||||
isM: true,
|
||||
span: { line: 1, start: 0, end: 5 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a C instruction with jump", () => {
|
||||
const match = grammar.match("D;JEQ", "cInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
jump: JUMP.asm["JEQ"],
|
||||
isM: false,
|
||||
span: { line: 1, start: 0, end: 5 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a C instruction with assignment and jump", () => {
|
||||
const match = grammar.match("A=D;JEQ", "cInstruction");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(asmSemantics(match).instruction).toEqual({
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
jump: JUMP.asm["JEQ"],
|
||||
store: ASSIGN.asm["A"],
|
||||
isM: false,
|
||||
span: { line: 1, start: 0, end: 7 },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a file into instructions", () => {
|
||||
const match = grammar.match(MaxAsm);
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
const { instructions } = asmSemantics(match).asm as Asm;
|
||||
expect(instructions).toEqual([
|
||||
{
|
||||
type: "A",
|
||||
label: "R0",
|
||||
span: { line: 10, start: 319, end: 322 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 11, start: 325, end: 328 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "R1",
|
||||
span: { line: 12, start: 331, end: 334 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D-M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 13, start: 337, end: 342 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "ITSR0",
|
||||
span: { line: 15, start: 372, end: 378 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
jump: JUMP.asm["JGT"],
|
||||
isM: false,
|
||||
span: { line: 16, start: 381, end: 386 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "R1",
|
||||
span: { line: 18, start: 401, end: 404 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 19, start: 407, end: 410 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "OUTPUT_D",
|
||||
span: { line: 20, start: 413, end: 422 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("0"),
|
||||
jump: JUMP.asm["JMP"],
|
||||
isM: false,
|
||||
span: { line: 21, start: 425, end: 430 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "ITSR0",
|
||||
span: { line: 22, start: 431, end: 438 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "R0",
|
||||
span: { line: 23, start: 441, end: 444 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 24, start: 447, end: 450 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "OUTPUT_D",
|
||||
span: { line: 25, start: 451, end: 461 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "R2",
|
||||
span: { line: 26, start: 464, end: 467 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
store: ASSIGN.asm["M"],
|
||||
isM: false,
|
||||
span: { line: 27, start: 470, end: 473 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "END",
|
||||
span: { line: 28, start: 474, end: 479 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
label: "END",
|
||||
span: { line: 29, start: 482, end: 486 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("0"),
|
||||
jump: JUMP.asm["JMP"],
|
||||
isM: false,
|
||||
span: { line: 30, start: 489, end: 494 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("assembles a file to hack", () => {
|
||||
const match = grammar.match(MaxAsm);
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
|
||||
const asm: Asm = asmSemantics(match).asm;
|
||||
fillLabel(asm);
|
||||
|
||||
expect(asm.instructions).toEqual([
|
||||
{
|
||||
type: "A",
|
||||
value: 0,
|
||||
span: { line: 10, start: 319, end: 322 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 11, start: 325, end: 328 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 1,
|
||||
span: { line: 12, start: 331, end: 334 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D-M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 13, start: 337, end: 342 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 10,
|
||||
span: { line: 15, start: 372, end: 378 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
jump: JUMP.asm["JGT"],
|
||||
isM: false,
|
||||
span: { line: 16, start: 381, end: 386 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 1,
|
||||
span: { line: 18, start: 401, end: 404 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 19, start: 407, end: 410 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 12,
|
||||
span: { line: 20, start: 413, end: 422 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("0"),
|
||||
jump: JUMP.asm["JMP"],
|
||||
isM: false,
|
||||
span: { line: 21, start: 425, end: 430 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "ITSR0",
|
||||
span: { line: 22, start: 431, end: 438 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 0,
|
||||
span: { line: 23, start: 441, end: 444 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("M"),
|
||||
store: ASSIGN.asm["D"],
|
||||
isM: true,
|
||||
span: { line: 24, start: 447, end: 450 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "OUTPUT_D",
|
||||
span: { line: 25, start: 451, end: 461 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 2,
|
||||
span: { line: 26, start: 464, end: 467 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("D"),
|
||||
store: ASSIGN.asm["M"],
|
||||
isM: false,
|
||||
span: { line: 27, start: 470, end: 473 },
|
||||
},
|
||||
{
|
||||
type: "L",
|
||||
label: "END",
|
||||
span: { line: 28, start: 474, end: 479 },
|
||||
},
|
||||
{
|
||||
type: "A",
|
||||
value: 14,
|
||||
span: { line: 29, start: 482, end: 486 },
|
||||
},
|
||||
{
|
||||
type: "C",
|
||||
op: COMMANDS.getOp("0"),
|
||||
jump: JUMP.asm["JMP"],
|
||||
isM: false,
|
||||
span: { line: 30, start: 489, end: 494 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("assembles a file to bin", () => {
|
||||
const match = grammar.match(MaxAsm);
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
|
||||
const asm: Asm = asmSemantics(match).asm;
|
||||
fillLabel(asm);
|
||||
const bin = emit(asm);
|
||||
|
||||
// biome-ignore format: special constant formatting
|
||||
const file = [
|
||||
0b0_000000000000000, // @R0 0x0000
|
||||
0b111_1_110000_010_000, // D=M 0xFE10
|
||||
0b0_000000000000001, // @R1 0x0001
|
||||
0b111_1_010011_010_000, // D=D-M 0xF8D0
|
||||
0b0_000000000001010, // @ITSR0#10 0x000A
|
||||
0b111_0_001100_000_001, // D;JGT 0xE301
|
||||
0b0_000000000000001, // @R1 0x0001
|
||||
0b111_1_110000_010_000, // D=M 0xFE10
|
||||
0b0_000000000001100, // @OUTPUT_D#12 0x000C
|
||||
0b111_0_101010_000_111, // 0;JMP (ITSR0:10) 0xEA85
|
||||
0b0_000000000000000, // @R0 0x0000
|
||||
0b111_1_110000_010_000, // D=M (OUTPUT_D:12) 0x000C
|
||||
0b0_000000000000010, // @R2 0x0002
|
||||
0b111_0_001100_001_000, // M=D (INFINITE LOOP:14) 0xE308
|
||||
0b0_000000000001110, // @INFINITE_LOOP#14 0x0014
|
||||
0b111_0_101010_000_111, // 0;JMP 0xEA83
|
||||
];
|
||||
expect(bin).toEqual(file);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,365 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { type Node, grammar as ohmGrammar } from "ohm-js";
|
||||
import {
|
||||
ASSIGN,
|
||||
ASSIGN_ASM,
|
||||
ASSIGN_OP,
|
||||
COMMANDS,
|
||||
COMMANDS_ASM,
|
||||
COMMANDS_OP,
|
||||
isAssignAsm,
|
||||
isCommandAsm,
|
||||
JUMP,
|
||||
JUMP_ASM,
|
||||
JUMP_OP,
|
||||
} from "../cpu/alu.js";
|
||||
import { KEYBOARD_OFFSET, SCREEN_OFFSET } from "../cpu/memory.js";
|
||||
import { makeC } from "../util/asm.js";
|
||||
import {
|
||||
baseSemantics,
|
||||
CompilationError,
|
||||
createError,
|
||||
grammars,
|
||||
makeParser,
|
||||
Span,
|
||||
span,
|
||||
} from "./base.js";
|
||||
import asmGrammar from "./grammars/asm.ohm.js";
|
||||
|
||||
export const grammar = ohmGrammar(asmGrammar, grammars);
|
||||
export const asmSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
export interface Asm {
|
||||
instructions: AsmInstruction[];
|
||||
}
|
||||
|
||||
export type AsmInstruction =
|
||||
| AsmAInstruction
|
||||
| AsmCInstruction
|
||||
| AsmLabelInstruction;
|
||||
|
||||
export type AsmAInstruction = AsmALabelInstruction | AsmAValueInstruction;
|
||||
export interface AsmALabelInstruction {
|
||||
type: "A";
|
||||
label: string;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
export interface AsmAValueInstruction {
|
||||
type: "A";
|
||||
value: number;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
export function isAValueInstruction(
|
||||
inst: AsmInstruction,
|
||||
): inst is AsmAValueInstruction {
|
||||
return inst.type == "A" && (inst as AsmAValueInstruction).value !== undefined;
|
||||
}
|
||||
|
||||
function isALabelInstruction(
|
||||
inst: AsmInstruction,
|
||||
): inst is AsmALabelInstruction {
|
||||
return inst.type == "A" && (inst as AsmALabelInstruction).label !== undefined;
|
||||
}
|
||||
|
||||
export interface AsmCInstruction {
|
||||
type: "C";
|
||||
op: COMMANDS_OP;
|
||||
isM: boolean;
|
||||
store?: ASSIGN_OP;
|
||||
jump?: JUMP_OP;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
export interface AsmLabelInstruction {
|
||||
type: "L";
|
||||
label: string;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
asmSemantics.addAttribute<Asm>("root", {
|
||||
Root(_) {
|
||||
return this.asm;
|
||||
},
|
||||
});
|
||||
|
||||
asmSemantics.addAttribute<Asm>("asm", {
|
||||
ASM(asm, last) {
|
||||
const instructions =
|
||||
asm.children.map(
|
||||
(node) => node.intermediateInstruction as AsmInstruction,
|
||||
) ?? [];
|
||||
return {
|
||||
instructions: last.child(0)
|
||||
? [...instructions, last.child(0).instruction]
|
||||
: instructions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
asmSemantics.addAttribute<AsmInstruction>("intermediateInstruction", {
|
||||
intermediateInstruction(inst, _n) {
|
||||
return inst.instruction;
|
||||
},
|
||||
});
|
||||
|
||||
function getAsmAssign(assignN: Node) {
|
||||
let assign = assignN.child(0)?.child(0)?.sourceString ?? "";
|
||||
|
||||
// The book (figure 4.5) specifies DM and ADM as the correct forms for destination,
|
||||
// but since the desktop simulators accept only MD and AMD we have decided to accept both */
|
||||
if (assign == "DM") {
|
||||
assign = "MD";
|
||||
}
|
||||
if (assign == "ADM") {
|
||||
assign = "AMD";
|
||||
}
|
||||
if (assign != "" && !isAssignAsm(assign)) {
|
||||
const reversed = assign.split("").reverse().join("");
|
||||
const suggestion = isAssignAsm(reversed)
|
||||
? `. Did you mean ${reversed}?`
|
||||
: "";
|
||||
throw createError(
|
||||
`Invalid ASM target: ${assign}${suggestion}`,
|
||||
span(assignN.source),
|
||||
);
|
||||
}
|
||||
|
||||
return assign;
|
||||
}
|
||||
|
||||
function getAsmOp(opN: Node) {
|
||||
const op = opN.sourceString;
|
||||
|
||||
if (!isCommandAsm(op)) {
|
||||
const reversed = op.split("").reverse().join("");
|
||||
const suggestion = isCommandAsm(reversed)
|
||||
? `. Did you mean ${reversed}?`
|
||||
: "";
|
||||
throw createError(
|
||||
`Invalid ASM value: ${opN.sourceString}${suggestion}`,
|
||||
span(opN.source),
|
||||
);
|
||||
}
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
asmSemantics.addAttribute<AsmInstruction>("instruction", {
|
||||
aInstruction(_at, name): AsmAInstruction {
|
||||
return A(name.value, span(this.source));
|
||||
},
|
||||
cInstruction(assignN, opN, jmpN): AsmCInstruction {
|
||||
const assign = getAsmAssign(assignN);
|
||||
|
||||
const op = getAsmOp(opN) as COMMANDS_ASM;
|
||||
const jmp = (jmpN.child(0)?.child(1)?.sourceString ?? "") as JUMP_ASM;
|
||||
return C(assign as ASSIGN_ASM, op, jmp, span(this.source));
|
||||
},
|
||||
label(_o, { name }, _c): AsmLabelInstruction {
|
||||
return L(name, span(this.source));
|
||||
},
|
||||
});
|
||||
|
||||
export type Pointer =
|
||||
| "R0"
|
||||
| "R1"
|
||||
| "R2"
|
||||
| "R3"
|
||||
| "R4"
|
||||
| "R5"
|
||||
| "R6"
|
||||
| "R7"
|
||||
| "R8"
|
||||
| "R9"
|
||||
| "R10"
|
||||
| "R11"
|
||||
| "R12"
|
||||
| "R13"
|
||||
| "R14"
|
||||
| "R15"
|
||||
| "SP"
|
||||
| "LCL"
|
||||
| "ARG"
|
||||
| "THIS"
|
||||
| "THAT"
|
||||
| "SCREEN"
|
||||
| "KBD";
|
||||
|
||||
export function fillLabel(
|
||||
asm: Asm,
|
||||
symbolCallback?: (name: string, value: number, isVar: boolean) => void,
|
||||
): Result<void, CompilationError> {
|
||||
let nextLabel = 16;
|
||||
const symbols = new Map<Pointer | string, number>([
|
||||
["R0", 0],
|
||||
["R1", 1],
|
||||
["R2", 2],
|
||||
["R3", 3],
|
||||
["R4", 4],
|
||||
["R5", 5],
|
||||
["R6", 6],
|
||||
["R7", 7],
|
||||
["R8", 8],
|
||||
["R9", 9],
|
||||
["R10", 10],
|
||||
["R11", 11],
|
||||
["R12", 12],
|
||||
["R13", 13],
|
||||
["R14", 14],
|
||||
["R15", 15],
|
||||
["SP", 0],
|
||||
["LCL", 1],
|
||||
["ARG", 2],
|
||||
["THIS", 3],
|
||||
["THAT", 4],
|
||||
["SCREEN", SCREEN_OFFSET],
|
||||
["KBD", KEYBOARD_OFFSET],
|
||||
]);
|
||||
|
||||
function getLabelValue(label: string) {
|
||||
if (!symbols.has(label)) {
|
||||
symbols.set(label, nextLabel);
|
||||
symbolCallback?.(label, nextLabel, true);
|
||||
nextLabel += 1;
|
||||
}
|
||||
return assertExists(symbols.get(label), `Label not in symbols: ${label}`);
|
||||
}
|
||||
|
||||
function transmuteAInstruction(instruction: AsmALabelInstruction) {
|
||||
const value = getLabelValue(instruction.label);
|
||||
(instruction as unknown as AsmAValueInstruction).value = value;
|
||||
delete (instruction as unknown as { label: undefined }).label;
|
||||
}
|
||||
|
||||
const unfilled: AsmALabelInstruction[] = [];
|
||||
let line = 0;
|
||||
for (const instruction of asm.instructions) {
|
||||
if (instruction.type === "L") {
|
||||
if (symbols.has(instruction.label)) {
|
||||
return Err(
|
||||
createError(`Duplicate label ${instruction.label}`, instruction.span),
|
||||
);
|
||||
} else {
|
||||
symbols.set(instruction.label, line);
|
||||
symbolCallback?.(instruction.label, line, false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
line += 1;
|
||||
|
||||
if (instruction.type === "A") {
|
||||
if (isALabelInstruction(instruction)) {
|
||||
unfilled.push(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unfilled.forEach(transmuteAInstruction);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
function writeCInst(inst: AsmCInstruction): string {
|
||||
return (
|
||||
(inst.store ? `${ASSIGN.op[inst.store]}=` : "") +
|
||||
COMMANDS.op[inst.op] +
|
||||
(inst.jump ? `;${JUMP.op[inst.jump]}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
export const AsmToString = (inst: AsmInstruction | string): string => {
|
||||
if (typeof inst === "string") return inst;
|
||||
switch (inst.type) {
|
||||
case "A":
|
||||
return isALabelInstruction(inst) ? `@${inst.label}` : `@${inst.value}`;
|
||||
case "L":
|
||||
return `(${inst.label})`;
|
||||
case "C":
|
||||
return writeCInst(inst);
|
||||
}
|
||||
};
|
||||
|
||||
export function translateInstruction(inst: AsmInstruction): number | undefined {
|
||||
if (inst.type === "A") {
|
||||
if (isALabelInstruction(inst)) {
|
||||
throw new Error(`ASM Emitting unfilled A instruction`);
|
||||
}
|
||||
return inst.value;
|
||||
}
|
||||
if (inst.type === "C") {
|
||||
return makeC(
|
||||
inst.isM,
|
||||
inst.op,
|
||||
(inst.store ?? 0) as ASSIGN_OP,
|
||||
(inst.jump ?? 0) as ASSIGN_OP,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function emit(asm: Asm): number[] {
|
||||
return asm.instructions
|
||||
.map(translateInstruction)
|
||||
.filter((op): op is number => op !== undefined);
|
||||
}
|
||||
|
||||
const A = (source: string | number, span?: Span): AsmAInstruction =>
|
||||
typeof source === "string"
|
||||
? {
|
||||
type: "A",
|
||||
label: source,
|
||||
span,
|
||||
}
|
||||
: {
|
||||
type: "A",
|
||||
value: source,
|
||||
span,
|
||||
};
|
||||
|
||||
const C = (
|
||||
assign: ASSIGN_ASM,
|
||||
op: COMMANDS_ASM,
|
||||
jmp?: JUMP_ASM,
|
||||
span?: Span,
|
||||
): AsmCInstruction => {
|
||||
const inst: AsmCInstruction = {
|
||||
type: "C",
|
||||
op: COMMANDS.getOp(op),
|
||||
isM: op.includes("M"),
|
||||
span,
|
||||
};
|
||||
if (jmp) inst.jump = JUMP.asm[jmp];
|
||||
if (assign) inst.store = ASSIGN.asm[assign];
|
||||
return inst;
|
||||
};
|
||||
|
||||
const AC = (
|
||||
source: string | number,
|
||||
assign: ASSIGN_ASM,
|
||||
op: COMMANDS_ASM,
|
||||
jmp?: JUMP_ASM,
|
||||
) => [A(source), C(assign, op, jmp)];
|
||||
|
||||
const L = (label: string, span?: Span): AsmLabelInstruction => ({
|
||||
type: "L",
|
||||
label,
|
||||
span,
|
||||
});
|
||||
|
||||
export const ASM = {
|
||||
grammar: asmGrammar,
|
||||
semantics: asmSemantics,
|
||||
parser: grammar,
|
||||
parse: makeParser<Asm>(grammar, asmSemantics),
|
||||
passes: {
|
||||
fillLabel,
|
||||
emit,
|
||||
},
|
||||
A,
|
||||
C,
|
||||
AC,
|
||||
L,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { cleanState } from "@davidsouther/jiffies/lib/esm/scope/state.js";
|
||||
import { grammar } from "ohm-js";
|
||||
import { baseSemantics, grammars } from "./base.js";
|
||||
|
||||
describe("Ohm Base", () => {
|
||||
it("parses numbers", () => {
|
||||
const match = grammars.Base.match("1234", "Number");
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
const { value } = baseSemantics(match);
|
||||
expect(value).toBe(1234);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["%XFF", 255],
|
||||
["%D128", 128],
|
||||
["127", 127],
|
||||
["%B11", 3],
|
||||
["%D-1", 0xffff],
|
||||
["0", 0],
|
||||
["11111", 11111],
|
||||
])("parses values", (str, num) => {
|
||||
const match = grammars.Base.match(str, "Number");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(baseSemantics(match).value).toBe(num);
|
||||
});
|
||||
|
||||
it("saves names", () => {
|
||||
const match = grammars.Base.match("inout", "Name");
|
||||
expect(match).toHaveSucceeded();
|
||||
const { name } = baseSemantics(match);
|
||||
expect(name).toBe("inout");
|
||||
});
|
||||
|
||||
describe("trailing lists", () => {
|
||||
const state = cleanState(() => {
|
||||
const repGrammar = grammar(
|
||||
`Rep <: Base {
|
||||
Rep = List<"A", ",">
|
||||
Block = OpenParen Rep CloseParen
|
||||
}`,
|
||||
grammars,
|
||||
);
|
||||
return { repGrammar };
|
||||
}, beforeEach);
|
||||
|
||||
it.each([
|
||||
["A,", "Rep"],
|
||||
["(A,)", "Block"],
|
||||
])("allows trailing lists", (str, tag) => {
|
||||
expect(state.repGrammar.match(str, tag)).toHaveSucceeded();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
type Dict,
|
||||
type Grammar,
|
||||
grammar,
|
||||
Interval,
|
||||
type Semantics,
|
||||
} from "ohm-js";
|
||||
import { int2, int10, int16 } from "../util/twos.js";
|
||||
|
||||
import baseGrammar from "./grammars/base.ohm.js";
|
||||
export const grammars = {
|
||||
Base: grammar(baseGrammar),
|
||||
};
|
||||
|
||||
export const baseSemantics = grammars.Base.createSemantics();
|
||||
|
||||
baseSemantics.extendOperation("asIteration", {
|
||||
List(list, _) {
|
||||
return list.asIteration();
|
||||
},
|
||||
});
|
||||
|
||||
baseSemantics.addAttribute("value", {
|
||||
decNumber(_, digits): number {
|
||||
return int10(digits.sourceString);
|
||||
},
|
||||
wholeDec(_, digits): number {
|
||||
return int10(digits.sourceString);
|
||||
},
|
||||
binNumber(_, digits) {
|
||||
return int2(digits.sourceString);
|
||||
},
|
||||
hexNumber(_, digits) {
|
||||
return int16(digits.sourceString);
|
||||
},
|
||||
Number(num) {
|
||||
return num.value;
|
||||
},
|
||||
Name(ident) {
|
||||
return ident.name;
|
||||
},
|
||||
identifier(_, __): string {
|
||||
return this.sourceString;
|
||||
},
|
||||
});
|
||||
|
||||
baseSemantics.addAttribute("name", {
|
||||
identifier(_, __): string {
|
||||
return this.sourceString;
|
||||
},
|
||||
Name(_): string {
|
||||
return this.child(0)?.name;
|
||||
},
|
||||
});
|
||||
|
||||
baseSemantics.addAttribute("String", {
|
||||
String(_a, str, _b) {
|
||||
return str.sourceString;
|
||||
},
|
||||
});
|
||||
|
||||
export interface CompilationError {
|
||||
message: string;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
const UNKNOWN_HDL_ERROR = `HDL statement has a syntax error`;
|
||||
|
||||
export function createError(
|
||||
description: string,
|
||||
span?: Span,
|
||||
): CompilationError {
|
||||
const match = description.match(/Line \d+, col \d+: (?<message>.*)/);
|
||||
const message = match?.groups?.message ? match.groups.message : description;
|
||||
return {
|
||||
message: `${
|
||||
span?.line != undefined ? `Line ${span.line}: ` : ""
|
||||
}${message}`,
|
||||
span: span,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeParser<ResultType>(
|
||||
grammar: Grammar,
|
||||
semantics: Semantics,
|
||||
property: (obj: Dict) => ResultType = ({ root }) => root,
|
||||
): (source: string) => Result<ResultType, CompilationError> {
|
||||
return function parse(source) {
|
||||
try {
|
||||
const match = grammar.match(source);
|
||||
if (match.succeeded()) {
|
||||
const parsed = semantics(match);
|
||||
const parse = property(parsed);
|
||||
return Ok(parse);
|
||||
} else {
|
||||
return Err(
|
||||
createError(
|
||||
match.shortMessage ?? UNKNOWN_HDL_ERROR,
|
||||
span(match.getInterval()),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return Err(e as Error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Span {
|
||||
start: number;
|
||||
end: number;
|
||||
line: number;
|
||||
}
|
||||
|
||||
export function span(span: Interval): Span {
|
||||
return {
|
||||
start: span.startIdx,
|
||||
end: span.endIdx,
|
||||
line: span.getLineAndColumn().lineNum,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cmpSemantics, grammar } from "./cmp.js";
|
||||
|
||||
describe("cmp language", () => {
|
||||
it("parses an empty file", () => {
|
||||
const match = grammar.match("");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(cmpSemantics(match).root).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses a file into lines", () => {
|
||||
const match = grammar.match(`| a | b | out |
|
||||
| 0 | 0 | 0 |
|
||||
| 1 | 0 | 1 |
|
||||
| 0 | 1 | 1 |
|
||||
| 1 | 1 | 0 |`);
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(cmpSemantics(match).root).toEqual([
|
||||
[" a ", " b ", " out "],
|
||||
[" 0 ", " 0 ", " 0 "],
|
||||
[" 1 ", " 0 ", " 1 "],
|
||||
[" 0 ", " 1 ", " 1 "],
|
||||
[" 1 ", " 1 ", " 0 "],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { grammar as ohmGrammar } from "ohm-js";
|
||||
import { baseSemantics, grammars, makeParser } from "./base.js";
|
||||
|
||||
export type Cell = string;
|
||||
export type Line = Cell[];
|
||||
export type Cmp = Line[];
|
||||
|
||||
import cmpGrammar from "./grammars/cmp.ohm.js";
|
||||
export const grammar = ohmGrammar(cmpGrammar, grammars);
|
||||
export const cmpSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
cmpSemantics.addAttribute<Cell>("cell", {
|
||||
cell(value, _) {
|
||||
return value.sourceString;
|
||||
},
|
||||
});
|
||||
|
||||
cmpSemantics.addAttribute<Line>("line", {
|
||||
line(_a, cells, _b) {
|
||||
return cells.children.map((c) => c.cell);
|
||||
},
|
||||
});
|
||||
|
||||
cmpSemantics.addAttribute<Cmp>("root", {
|
||||
Root(lines) {
|
||||
return lines.children.map((c) => c.line);
|
||||
},
|
||||
});
|
||||
|
||||
export const CMP = {
|
||||
grammar: cmpGrammar,
|
||||
semantics: cmpSemantics,
|
||||
parser: grammar,
|
||||
parse: makeParser<Cmp>(grammar, cmpSemantics),
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
.ohm.js
|
||||
@@ -0,0 +1,21 @@
|
||||
ASM <: Base {
|
||||
Root := ASM
|
||||
ASM = intermediateInstruction* instruction?
|
||||
|
||||
instruction = label|aInstruction|cInstruction
|
||||
intermediateInstruction = instruction space+
|
||||
|
||||
identifier := (letter|underscore|dot|dollar|colon) (alnum|underscore|dot|dollar|colon)*
|
||||
|
||||
label = openParen identifier closeParen
|
||||
aInstruction = at (identifier | decNumber)
|
||||
cInstruction = assign? op jmp?
|
||||
|
||||
assignChar = "A" | "M" | "D"
|
||||
opChar = assignChar | "0" | "1" | "!" | "-" | "+" | "|" | "&"
|
||||
|
||||
assign = assignChar+ equal
|
||||
op = opChar+
|
||||
|
||||
jmp = semi ("JGT" | "JEQ" | "JGE" | "JLT" | "JNE" | "JLE" | "JMP")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const asm = `ASM <: Base {
|
||||
Root := ASM
|
||||
ASM = intermediateInstruction* instruction?
|
||||
|
||||
instruction = label|aInstruction|cInstruction
|
||||
intermediateInstruction = instruction space+
|
||||
|
||||
identifier := (letter|underscore|dot|dollar|colon) (alnum|underscore|dot|dollar|colon)*
|
||||
|
||||
label = openParen identifier closeParen
|
||||
aInstruction = at (identifier | decNumber)
|
||||
cInstruction = assign? op jmp?
|
||||
|
||||
assignChar = "A" | "M" | "D"
|
||||
opChar = assignChar | "0" | "1" | "!" | "-" | "+" | "|" | "&"
|
||||
|
||||
assign = assignChar+ equal
|
||||
op = opChar+
|
||||
|
||||
jmp = semi ("JGT" | "JEQ" | "JGE" | "JLT" | "JNE" | "JLE" | "JMP")
|
||||
}`;
|
||||
export default asm;
|
||||
@@ -0,0 +1,77 @@
|
||||
Base {
|
||||
Root = Value*
|
||||
|
||||
At = at
|
||||
Bang = bang
|
||||
Bar = bar
|
||||
CloseAngle = closeAngle
|
||||
CloseBrace = closeBrace
|
||||
CloseParen = closeParen
|
||||
CloseSquare = closeSquare
|
||||
Comma = comma
|
||||
Dollar = dollar
|
||||
Dot = dot
|
||||
DoubleQuote = doubleQuote
|
||||
Equal = equal
|
||||
OpenAngle = openAngle
|
||||
OpenBrace = openBrace
|
||||
OpenParen = openParen
|
||||
OpenSquare = openSquare
|
||||
Percent = percent
|
||||
Semi = semi
|
||||
Underscore = underscore
|
||||
|
||||
at = "@"
|
||||
bang = "!"
|
||||
bar = "|"
|
||||
closeAngle = ">"
|
||||
closeBrace = "}"
|
||||
closeParen = ")"
|
||||
closeSquare = "]"
|
||||
comma = ","
|
||||
dollar = "$"
|
||||
dot = "."
|
||||
doubleQuote = "\""
|
||||
equal = "="
|
||||
minus = "-"
|
||||
newline = "\r"? "\n"
|
||||
openAngle = "<"
|
||||
openBrace = "{"
|
||||
openParen = "("
|
||||
openSquare = "["
|
||||
percent = "%"
|
||||
semi = ";"
|
||||
underscore = "_"
|
||||
colon = ":"
|
||||
|
||||
Value = identifier | number | boolean
|
||||
|
||||
boolean = true | false
|
||||
True = true
|
||||
False = false
|
||||
true = "true"
|
||||
false = "false"
|
||||
|
||||
Name = identifier
|
||||
identifier = (letter|underscore|dot|dollar) (alnum|underscore|dot|dollar)*
|
||||
|
||||
Number = number
|
||||
number = hexNumber | decNumber | binNumber
|
||||
binNumber = ("%B") ("0"|"1")+
|
||||
hexNumber = ("%X") hexDigit+
|
||||
decNumber = ("%D")? (wholeDec | realDec)
|
||||
wholeDec = minus? digit+
|
||||
realDec = minus? digit* "." digit+
|
||||
|
||||
String = DoubleQuote (~doubleQuote any)* doubleQuote
|
||||
|
||||
spaces := (lineComment | comment | space)*
|
||||
commentStart = "/*"
|
||||
commentEnd = "*/"
|
||||
comment = commentStart (~commentEnd any)* commentEnd
|
||||
lineCommentStart = "//"
|
||||
lineComment = lineCommentStart (~"\n" any)*
|
||||
|
||||
List<elem, sep> = NonemptyListOf<elem, sep> sep?
|
||||
EmptyList<elem, sep> = EmptyList<elem, sep> sep?
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
const base = `
|
||||
Base {
|
||||
Root = Value*
|
||||
|
||||
At = at
|
||||
Bang = bang
|
||||
Bar = bar
|
||||
CloseAngle = closeAngle
|
||||
CloseBrace = closeBrace
|
||||
CloseParen = closeParen
|
||||
CloseSquare = closeSquare
|
||||
Comma = comma
|
||||
Dollar = dollar
|
||||
Dot = dot
|
||||
DoubleQuote = doubleQuote
|
||||
Equal = equal
|
||||
OpenAngle = openAngle
|
||||
OpenBrace = openBrace
|
||||
OpenParen = openParen
|
||||
OpenSquare = openSquare
|
||||
Percent = percent
|
||||
Semi = semi
|
||||
Underscore = underscore
|
||||
|
||||
at = "@"
|
||||
bang = "!"
|
||||
bar = "|"
|
||||
closeAngle = ">"
|
||||
closeBrace = "}"
|
||||
closeParen = ")"
|
||||
closeSquare = "]"
|
||||
comma = ","
|
||||
dollar = "$"
|
||||
dot = "."
|
||||
doubleQuote = "\\""
|
||||
equal = "="
|
||||
minus = "-"
|
||||
newline = "\\r"? "\\n"
|
||||
openAngle = "<"
|
||||
openBrace = "{"
|
||||
openParen = "("
|
||||
openSquare = "["
|
||||
percent = "%"
|
||||
semi = ";"
|
||||
underscore = "_"
|
||||
colon = ":"
|
||||
|
||||
Value = identifier | number | boolean
|
||||
|
||||
boolean = true | false
|
||||
True = true
|
||||
False = false
|
||||
true = "true"
|
||||
false = "false"
|
||||
|
||||
Name = identifier
|
||||
identifier = (letter|underscore|dot|dollar) (alnum|underscore|dot|dollar)*
|
||||
|
||||
Number = number
|
||||
number = hexNumber | decNumber | binNumber
|
||||
binNumber = ("%B") ("0"|"1")+
|
||||
hexNumber = ("%X") hexDigit+
|
||||
decNumber = ("%D")? (wholeDec | realDec)
|
||||
wholeDec = minus? digit+
|
||||
realDec = minus? digit* "." digit+
|
||||
|
||||
String = DoubleQuote (~doubleQuote any)* doubleQuote
|
||||
|
||||
spaces := (lineComment | comment | space)*
|
||||
commentStart = "/*"
|
||||
commentEnd = "*/"
|
||||
comment = commentStart (~commentEnd any)* commentEnd
|
||||
lineCommentStart = "//"
|
||||
lineComment = lineCommentStart (~"\\n" any)*
|
||||
|
||||
List<elem, sep> = NonemptyListOf<elem, sep> sep?
|
||||
EmptyList<elem, sep> = EmptyList<elem, sep> sep?
|
||||
}`;
|
||||
export default base;
|
||||
@@ -0,0 +1,6 @@
|
||||
Cmp <: Base {
|
||||
Root := line*
|
||||
line = bar cell+ newline?
|
||||
cell = cellvalue bar
|
||||
cellvalue = (~(bar|newline) any)*
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const cmp = `
|
||||
Cmp <: Base {
|
||||
Root := line*
|
||||
line = bar cell+ newline?
|
||||
cell = cellvalue bar
|
||||
cellvalue = (~(bar|newline) any)*
|
||||
}`;
|
||||
export default cmp;
|
||||
@@ -0,0 +1,23 @@
|
||||
Hdl <: Base{
|
||||
Root := Chip
|
||||
identifier := (letter) (alnum)*
|
||||
Name := identifier
|
||||
Chip = "CHIP" Name OpenBrace ChipBody CloseBrace
|
||||
ChipBody = InList? OutList? PartList ClockedList?
|
||||
InList = "IN" PinList Semi
|
||||
OutList = "OUT" PinList Semi
|
||||
PartList = BuiltinPart | Parts
|
||||
PinList = List<PinDecl, Comma>
|
||||
PinDecl = Name PinWidth?
|
||||
PinWidth = OpenSquare decNumber CloseSquare
|
||||
BuiltinPart = "BUILTIN" Semi
|
||||
Parts = "PARTS:" Part*
|
||||
Part = Name "(" Wires ")" Semi
|
||||
Wires = List<Wire, Comma>
|
||||
Wire = WireSide Equal (WireSide | True | False)
|
||||
WireSide = Name SubBus?
|
||||
SubBus = OpenSquare decNumber subBusRest? CloseSquare
|
||||
subBusRest = ".." decNumber
|
||||
ClockedList = "CLOCKED" SimplePinList Semi
|
||||
SimplePinList = List<Name, Comma>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const hdl = `
|
||||
Hdl <: Base{
|
||||
Root := Chip
|
||||
identifier := (letter) (alnum)*
|
||||
Name := identifier
|
||||
Chip = "CHIP" Name OpenBrace ChipBody CloseBrace
|
||||
ChipBody = InList? OutList? PartList ClockedList?
|
||||
InList = "IN" PinList Semi
|
||||
OutList = "OUT" PinList Semi
|
||||
PartList = BuiltinPart | Parts
|
||||
PinList = List<PinDecl, Comma>
|
||||
PinDecl = Name PinWidth?
|
||||
PinWidth = OpenSquare decNumber CloseSquare
|
||||
BuiltinPart = "BUILTIN" Semi
|
||||
Parts = "PARTS:" Part*
|
||||
Part = Name "(" Wires ")" Semi
|
||||
Wires = List<Wire, Comma>
|
||||
Wire = WireSide Equal (WireSide | True | False)
|
||||
WireSide = Name SubBus?
|
||||
SubBus = OpenSquare decNumber subBusRest? CloseSquare
|
||||
subBusRest = ".." decNumber
|
||||
ClockedList = "CLOCKED" SimplePinList Semi
|
||||
SimplePinList = List<Name, Comma>
|
||||
}`;
|
||||
export default hdl;
|
||||
@@ -0,0 +1,78 @@
|
||||
Jack <: Base {
|
||||
Root := Class
|
||||
|
||||
whitespace = (lineComment | comment | space)
|
||||
|
||||
class = "class" whitespace+
|
||||
Class = class jackIdentifier OpenBrace ClassVarDec* SubroutineDec* CloseBrace
|
||||
|
||||
type = ("int" | "char" | "boolean" | jackIdentifier) whitespace+
|
||||
|
||||
classVarType = ("static" | "field") whitespace+
|
||||
ClassVarDec = classVarType type jackIdentifier TrailingIdentifier* Semi
|
||||
TrailingIdentifier = Comma jackIdentifier
|
||||
|
||||
void = "void" whitespace+
|
||||
returnType = (type | void)
|
||||
subroutineType = ("constructor" | "function" | "method") whitespace+
|
||||
SubroutineDec = subroutineType returnType jackIdentifier OpenParen ParameterList CloseParen SubroutineBody
|
||||
|
||||
|
||||
Parameter = type jackIdentifier
|
||||
Parameters = Parameter TrailingParameter*
|
||||
TrailingParameter = Comma Parameter
|
||||
ParameterList = Parameters?
|
||||
|
||||
SubroutineBody = OpenBrace VarDec* Statement* CloseBrace
|
||||
|
||||
var = "var" whitespace+
|
||||
VarDec = var type jackIdentifier TrailingIdentifier* Semi
|
||||
|
||||
Statement = LetStatement | IfStatement | WhileStatement | DoStatement | ReturnStatement
|
||||
|
||||
arrayAccessStart = jackIdentifier openSquare
|
||||
ArrayAccess = arrayAccessStart Expression CloseSquare
|
||||
|
||||
let = "let" whitespace+
|
||||
LetTarget = ArrayAccess | jackIdentifier
|
||||
LetStatement = let LetTarget Equal Expression Semi
|
||||
|
||||
IfStatement = "if" OpenParen Expression CloseParen OpenBrace Statement* CloseBrace ElseBlock?
|
||||
ElseBlock = "else" OpenBrace Statement* CloseBrace
|
||||
|
||||
WhileStatement = "while" OpenParen Expression CloseParen OpenBrace Statement* CloseBrace
|
||||
|
||||
do = "do" whitespace+
|
||||
DoStatement = do SubroutineCall Semi
|
||||
|
||||
return = "return"
|
||||
returnWithSpace = "return" whitespace+
|
||||
ReturnStatement = EmptyReturn | ReturnValue
|
||||
EmptyReturn = return Semi
|
||||
ReturnValue = returnWithSpace Expression Semi
|
||||
|
||||
op = "+" | "-" | "*" | "/" | "&" | "|" | "<" | ">" | "="
|
||||
ExpressionPart = op Term
|
||||
Expression = Term ExpressionPart*
|
||||
|
||||
integerConstant = digit+
|
||||
stringConstant = doubleQuote (~doubleQuote ~newline any)* doubleQuote
|
||||
keywordConstant = "true" | "false" | "null" | "this"
|
||||
|
||||
GroupedExpression = OpenParen Expression CloseParen
|
||||
|
||||
unaryOp = "-" | "~"
|
||||
UnaryExpression = unaryOp Term
|
||||
|
||||
Term = integerConstant | stringConstant | keywordConstant | SubroutineCall | ArrayAccess | jackIdentifier | GroupedExpression | UnaryExpression
|
||||
|
||||
compoundIdentifier = jackIdentifier dot jackIdentifier
|
||||
SubroutineName = compoundIdentifier | jackIdentifier
|
||||
SubroutineCall = SubroutineName OpenParen ExpressionList CloseParen
|
||||
|
||||
ExpressionList = Expressions?
|
||||
Expressions = Expression TrailingExpression*
|
||||
TrailingExpression = Comma Expression
|
||||
|
||||
jackIdentifier = letter (alnum | underscore)*
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
const jack = `Jack <: Base {
|
||||
Root := Class
|
||||
|
||||
whitespace = (lineComment | comment | space)
|
||||
|
||||
class = "class" whitespace+
|
||||
Class = class jackIdentifier OpenBrace ClassVarDec* SubroutineDec* CloseBrace
|
||||
|
||||
type = ("int" | "char" | "boolean" | jackIdentifier) whitespace+
|
||||
|
||||
classVarType = ("static" | "field") whitespace+
|
||||
ClassVarDec = classVarType type jackIdentifier TrailingIdentifier* Semi
|
||||
TrailingIdentifier = Comma jackIdentifier
|
||||
|
||||
void = "void" whitespace+
|
||||
returnType = (type | void)
|
||||
subroutineType = ("constructor" | "function" | "method") whitespace+
|
||||
SubroutineDec = subroutineType returnType jackIdentifier OpenParen ParameterList CloseParen SubroutineBody
|
||||
|
||||
|
||||
Parameter = type jackIdentifier
|
||||
Parameters = Parameter TrailingParameter*
|
||||
TrailingParameter = Comma Parameter
|
||||
ParameterList = Parameters?
|
||||
|
||||
SubroutineBody = OpenBrace VarDec* Statement* CloseBrace
|
||||
|
||||
var = "var" whitespace+
|
||||
VarDec = var type jackIdentifier TrailingIdentifier* Semi
|
||||
|
||||
Statement = LetStatement | IfStatement | WhileStatement | DoStatement | ReturnStatement
|
||||
|
||||
arrayAccessStart = jackIdentifier openSquare
|
||||
ArrayAccess = arrayAccessStart Expression CloseSquare
|
||||
|
||||
let = "let" whitespace+
|
||||
LetTarget = ArrayAccess | jackIdentifier
|
||||
LetStatement = let LetTarget Equal Expression Semi
|
||||
|
||||
IfStatement = "if" OpenParen Expression CloseParen OpenBrace Statement* CloseBrace ElseBlock?
|
||||
ElseBlock = "else" OpenBrace Statement* CloseBrace
|
||||
|
||||
WhileStatement = "while" OpenParen Expression CloseParen OpenBrace Statement* CloseBrace
|
||||
|
||||
do = "do" whitespace+
|
||||
DoStatement = do SubroutineCall Semi
|
||||
|
||||
return = "return"
|
||||
returnWithSpace = "return" whitespace+
|
||||
ReturnStatement = EmptyReturn | ReturnValue
|
||||
EmptyReturn = return Semi
|
||||
ReturnValue = returnWithSpace Expression Semi
|
||||
|
||||
op = "+" | "-" | "*" | "/" | "&" | "|" | "<" | ">" | "="
|
||||
ExpressionPart = op Term
|
||||
Expression = Term ExpressionPart*
|
||||
|
||||
integerConstant = digit+
|
||||
stringConstant = doubleQuote (~doubleQuote ~newline any)* doubleQuote
|
||||
keywordConstant = "true" | "false" | "null" | "this"
|
||||
|
||||
GroupedExpression = OpenParen Expression CloseParen
|
||||
|
||||
unaryOp = "-" | "~"
|
||||
UnaryExpression = unaryOp Term
|
||||
|
||||
Term = integerConstant | stringConstant | keywordConstant | SubroutineCall | ArrayAccess | jackIdentifier | GroupedExpression | UnaryExpression
|
||||
|
||||
compoundIdentifier = jackIdentifier dot jackIdentifier
|
||||
SubroutineName = compoundIdentifier | jackIdentifier
|
||||
SubroutineCall = SubroutineName OpenParen ExpressionList CloseParen
|
||||
|
||||
ExpressionList = Expressions?
|
||||
Expressions = Expression TrailingExpression*
|
||||
TrailingExpression = Comma Expression
|
||||
|
||||
jackIdentifier = letter (alnum | underscore)*
|
||||
}`;
|
||||
|
||||
export default jack;
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")"
|
||||
|
||||
for f in *.ohm ; do
|
||||
echo "const ${f%.ohm} = \`" >| "${f}.js"
|
||||
cat "$f" | sed 's!\\!\\\\!g' >> "${f}.js"
|
||||
echo "\`;" >> "${f}.js"
|
||||
echo "export default ${f%.ohm};" >> "${f}.js"
|
||||
done
|
||||
@@ -0,0 +1,59 @@
|
||||
Tst <: Base {
|
||||
Root := Tst
|
||||
Tst = (TstStatement | TstRepeat | TstWhile)+
|
||||
|
||||
TstRepeat = Repeat Number? OpenBrace TstCommand+ CloseBrace
|
||||
TstWhile = While Condition OpenBrace TstCommand+ CloseBrace
|
||||
TstStatement = TstCommand
|
||||
|
||||
TstCommand = TstOperation Separator
|
||||
Separator = (Semi | Bang | Comma)
|
||||
|
||||
TstOperation =
|
||||
| TstFileOperation
|
||||
| TstOutputListOperation
|
||||
| TstEvalOperation
|
||||
| TstSetOperation
|
||||
| TstOutputOperation
|
||||
| TstEchoOperation
|
||||
| TstClearEchoOperation
|
||||
| TstLoadROMOperation
|
||||
| TstResetRAMOperation
|
||||
|
||||
TstLoadROMOperation = ROM32K Load FileName
|
||||
TstFileOperation = FileOperation FileName?
|
||||
TstOutputListOperation = "output-list" OutputFormat+
|
||||
OutputFormat = Name Index? FormatSpec?
|
||||
FormatSpec = percent FormatStyle wholeDec dot wholeDec dot wholeDec
|
||||
TstSetOperation = Set Name Index? Number
|
||||
Index = OpenSquare wholeDec? CloseSquare
|
||||
Condition = Value CompareOp Value
|
||||
TstEvalOperation = Eval | TickTock | Tick | Tock | VmStep
|
||||
TstOutputOperation = Output
|
||||
TstEchoOperation = Echo String
|
||||
TstClearEchoOperation = ClearEcho
|
||||
TstResetRAMOperation = ResetRAM
|
||||
|
||||
filename = (alnum|underscore|dot|dollar|minus)+
|
||||
FileName = filename
|
||||
FileOperation = "load" | "output-file" | "compare-to"
|
||||
|
||||
Set = "set"
|
||||
Eval = "eval"
|
||||
Tick = "tick"
|
||||
Tock = "tock"
|
||||
TickTock = "ticktock"
|
||||
VmStep = "vmstep"
|
||||
Echo = "echo"
|
||||
Repeat = "repeat"
|
||||
ClearEcho = "clear-echo"
|
||||
Output = "output"
|
||||
OutputList = "output-list"
|
||||
FormatStyle = "B"|"D"|"S"|"X"
|
||||
ROM32K = "ROM32K"
|
||||
Load = "load"
|
||||
While = "while"
|
||||
ResetRAM = "resetRam"
|
||||
|
||||
CompareOp = "<>" | "<=" | ">=" | "=" | "<" | ">"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const tst = `
|
||||
Tst <: Base {
|
||||
Root := Tst
|
||||
Tst = (TstStatement | TstRepeat | TstWhile)+
|
||||
|
||||
TstRepeat = Repeat Number? OpenBrace TstCommand+ CloseBrace
|
||||
TstWhile = While Condition OpenBrace TstCommand+ CloseBrace
|
||||
TstStatement = TstCommand
|
||||
|
||||
TstCommand = TstOperation Separator
|
||||
Separator = (Semi | Bang | Comma)
|
||||
|
||||
TstOperation =
|
||||
| TstFileOperation
|
||||
| TstOutputListOperation
|
||||
| TstEvalOperation
|
||||
| TstSetOperation
|
||||
| TstOutputOperation
|
||||
| TstEchoOperation
|
||||
| TstClearEchoOperation
|
||||
| TstLoadROMOperation
|
||||
| TstResetRAMOperation
|
||||
|
||||
TstLoadROMOperation = ROM32K Load FileName
|
||||
TstFileOperation = FileOperation FileName?
|
||||
TstOutputListOperation = "output-list" OutputFormat+
|
||||
OutputFormat = Name Index? FormatSpec?
|
||||
FormatSpec = percent FormatStyle wholeDec dot wholeDec dot wholeDec
|
||||
TstSetOperation = Set Name Index? Number
|
||||
Index = OpenSquare wholeDec? CloseSquare
|
||||
Condition = Value CompareOp Value
|
||||
TstEvalOperation = Eval | TickTock | Tick | Tock | VmStep
|
||||
TstOutputOperation = Output
|
||||
TstEchoOperation = Echo String
|
||||
TstClearEchoOperation = ClearEcho
|
||||
TstResetRAMOperation = ResetRAM
|
||||
|
||||
filename = (alnum|underscore|dot|dollar|minus)+
|
||||
FileName = filename
|
||||
FileOperation = "load" | "output-file" | "compare-to"
|
||||
|
||||
Set = "set"
|
||||
Eval = "eval"
|
||||
Tick = "tick"
|
||||
Tock = "tock"
|
||||
TickTock = "ticktock"
|
||||
VmStep = "vmstep"
|
||||
Echo = "echo"
|
||||
Repeat = "repeat"
|
||||
ClearEcho = "clear-echo"
|
||||
Output = "output"
|
||||
OutputList = "output-list"
|
||||
FormatStyle = "B"|"D"|"S"|"X"
|
||||
ROM32K = "ROM32K"
|
||||
Load = "load"
|
||||
While = "while"
|
||||
ResetRAM = "resetRam"
|
||||
|
||||
CompareOp = "<>" | "<=" | ">=" | "=" | "<" | ">"
|
||||
}`;
|
||||
export default tst;
|
||||
@@ -0,0 +1,56 @@
|
||||
Vm <: Base {
|
||||
Root := Vm
|
||||
|
||||
Vm = newline* VmInstructionLine* VmInstruction?
|
||||
|
||||
space := comment | " " | "\t"
|
||||
whitespace = lineComment | comment | space
|
||||
|
||||
VmInstructionLine = VmInstruction newline+
|
||||
VmInstruction =
|
||||
| StackInstruction
|
||||
| OpInstruction
|
||||
| FunctionInstruction
|
||||
| CallInstruction
|
||||
| ReturnInstruction
|
||||
| GotoInstruction
|
||||
| LabelInstruction
|
||||
|
||||
StackInstruction = (push | pop) MemorySegment Number
|
||||
OpInstruction = Add | Sub | Neg | Lt | Gt | Eq | And | Or | Not
|
||||
FunctionInstruction = function Name Number
|
||||
CallInstruction = call Name Number
|
||||
ReturnInstruction = return
|
||||
LabelInstruction = label Name
|
||||
GotoInstruction = (goto | ifGoto) Name
|
||||
|
||||
MemorySegment = argument | local | static | constant | this | that | pointer | temp
|
||||
|
||||
push = "push" whitespace+
|
||||
pop = "pop" whitespace+
|
||||
function = "function" whitespace+
|
||||
call = "call" whitespace+
|
||||
return = "return"
|
||||
goto = "goto" whitespace+
|
||||
ifGoto = "if-goto" whitespace+
|
||||
label = "label" whitespace+
|
||||
|
||||
argument = "argument" whitespace+
|
||||
local = "local" whitespace+
|
||||
static = "static" whitespace+
|
||||
constant = "constant" whitespace+
|
||||
this = "this" whitespace+
|
||||
that = "that" whitespace+
|
||||
pointer = "pointer" whitespace+
|
||||
temp = "temp" whitespace+
|
||||
|
||||
Add = "add"
|
||||
Sub = "sub"
|
||||
Neg = "neg"
|
||||
Eq = "eq"
|
||||
Lt = "lt"
|
||||
Gt = "gt"
|
||||
And = "and"
|
||||
Or = "or"
|
||||
Not = "not"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
const vm = `Vm <: Base {
|
||||
Root := Vm
|
||||
|
||||
Vm = newline* VmInstructionLine* VmInstruction?
|
||||
|
||||
space := comment | " " | "\t"
|
||||
whitespace = lineComment | comment | space
|
||||
|
||||
VmInstructionLine = VmInstruction newline+
|
||||
VmInstruction =
|
||||
| StackInstruction
|
||||
| OpInstruction
|
||||
| FunctionInstruction
|
||||
| CallInstruction
|
||||
| ReturnInstruction
|
||||
| GotoInstruction
|
||||
| LabelInstruction
|
||||
|
||||
StackInstruction = (push | pop) MemorySegment Number
|
||||
OpInstruction = Add | Sub | Neg | Lt | Gt | Eq | And | Or | Not
|
||||
FunctionInstruction = function Name Number
|
||||
CallInstruction = call Name Number
|
||||
ReturnInstruction = return
|
||||
LabelInstruction = label Name
|
||||
GotoInstruction = (goto | ifGoto) Name
|
||||
|
||||
MemorySegment = argument | local | static | constant | this | that | pointer | temp
|
||||
|
||||
push = "push" whitespace+
|
||||
pop = "pop" whitespace+
|
||||
function = "function" whitespace+
|
||||
call = "call" whitespace+
|
||||
return = "return"
|
||||
goto = "goto" whitespace+
|
||||
ifGoto = "if-goto" whitespace+
|
||||
label = "label" whitespace+
|
||||
|
||||
argument = "argument" whitespace+
|
||||
local = "local" whitespace+
|
||||
static = "static" whitespace+
|
||||
constant = "constant" whitespace+
|
||||
this = "this" whitespace+
|
||||
that = "that" whitespace+
|
||||
pointer = "pointer" whitespace+
|
||||
temp = "temp" whitespace+
|
||||
|
||||
Add = "add"
|
||||
Sub = "sub"
|
||||
Neg = "neg"
|
||||
Eq = "eq"
|
||||
Lt = "lt"
|
||||
Gt = "gt"
|
||||
And = "and"
|
||||
Or = "or"
|
||||
Not = "not"
|
||||
}`;
|
||||
export default vm;
|
||||
@@ -0,0 +1,352 @@
|
||||
import {
|
||||
grammar,
|
||||
HdlParse,
|
||||
hdlSemantics,
|
||||
Part,
|
||||
PinDeclaration,
|
||||
} from "./hdl.js";
|
||||
|
||||
const AND_BUILTIN = `CHIP And {
|
||||
IN a, b;
|
||||
OUT out;
|
||||
BUILTIN;
|
||||
}`;
|
||||
|
||||
const NOT_PARTS = `CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
Nand(a=in, b=in, out=out);
|
||||
}`;
|
||||
|
||||
const NOT_NO_PARTS = `CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
}`;
|
||||
|
||||
const AND_16_BUILTIN = `CHIP And16 {
|
||||
IN a[16], b[16];
|
||||
OUT out[16];
|
||||
BUILTIN;
|
||||
}`;
|
||||
|
||||
const CLOCKED = `CHIP Foo {
|
||||
IN in;
|
||||
|
||||
PARTS:
|
||||
|
||||
CLOCKED in;
|
||||
}`;
|
||||
|
||||
const ERRORS = [
|
||||
["Not { BUILTIN }", 'Line 1, col 1: expected "CHIP"'],
|
||||
["CHIP { BUILTIN }", "Line 1, col 6: expected a letter"], // A chip name is expected
|
||||
["CHIP Not BUILTIN }", 'Line 1, col 10: expected "{"'],
|
||||
["CHIP Not { BUILTIN }", 'Line 1, col 20: expected ";"'],
|
||||
["CHIP Not { BONKERS; }", 'Line 1, col 12: expected "PARTS:" or "BUILTIN"'],
|
||||
["CHIP Not { ", 'Line 1, col 12: expected "PARTS:" or "BUILTIN"'],
|
||||
[
|
||||
"CHIP Not { PARTS: (); }",
|
||||
'Line 1, col 19: expected "}", "CLOCKED", or a letter', // A chip name is expected
|
||||
],
|
||||
["CHIP Not { PARTS: Nand; }", 'Line 1, col 23: expected "("'],
|
||||
["CHIP Not { PARTS: Nand() }", "Line 1, col 24: expected a letter"], // A pin name is expected
|
||||
["CHIP Not { PARTS: Nand(=a) }", "Line 1, col 24: expected a letter"], // A pin name is expected
|
||||
[
|
||||
"CHIP Not { PARTS: Nand(a=) }",
|
||||
'Line 1, col 26: expected "false", "true", or a letter',
|
||||
], // A pin name is expected
|
||||
["CHIP Not { PARTS: Nand(a) }", 'Line 1, col 25: expected "="'],
|
||||
["CHIP Not { PARTS: Nand(a=a }", 'Line 1, col 28: expected ")", ",", or "["'],
|
||||
];
|
||||
|
||||
describe("HDL w/ Ohm", () => {
|
||||
describe("parts", () => {
|
||||
it("parses part wires", () => {
|
||||
const wire = grammar.match("a[2..4]=b[10..12]", "Wire");
|
||||
expect(wire).toHaveSucceeded();
|
||||
expect<PinDeclaration>(hdlSemantics(wire).Wire).toEqual({
|
||||
lhs: {
|
||||
pin: "a",
|
||||
start: 2,
|
||||
end: 4,
|
||||
span: { start: 0, end: 7, line: 1 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "b",
|
||||
start: 10,
|
||||
end: 12,
|
||||
span: { start: 8, end: 17, line: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("parses parts", () => {
|
||||
const wide = grammar.match("Nand(a=a, b=b, out=out);", "Part");
|
||||
expect(wide).toHaveSucceeded();
|
||||
expect<Part>(hdlSemantics(wide).Part).toEqual({
|
||||
name: "Nand",
|
||||
span: { start: 0, end: 24, line: 1 },
|
||||
wires: [
|
||||
{
|
||||
lhs: {
|
||||
pin: "a",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 5, end: 6, line: 1 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "a",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 7, end: 8, line: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "b",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 10, end: 11, line: 1 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "b",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 12, end: 13, line: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "out",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 15, end: 18, line: 1 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "out",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 19, end: 22, line: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses trailing commas", () => {
|
||||
const parse1 = grammar.match(`a=a, b=b,`, "Wires");
|
||||
expect(parse1).toHaveSucceeded();
|
||||
const parse2 = grammar.match(`Foo(a=a, b=b,);`, "Part");
|
||||
expect(parse2).toHaveSucceeded();
|
||||
});
|
||||
|
||||
it("parses complex parts", () => {
|
||||
const not8 = grammar.match(
|
||||
`Not(in[0..1] = true,
|
||||
in[3..5] = six,
|
||||
in[7] = true,
|
||||
out[3..7] = out1,
|
||||
address=address[0..13],
|
||||
out[2..3]=address[5..6]);`,
|
||||
"Part",
|
||||
);
|
||||
expect(not8).toHaveSucceeded();
|
||||
expect<Part>(hdlSemantics(not8).Part).toEqual({
|
||||
name: "Not",
|
||||
span: { start: 0, end: 158, line: 1 },
|
||||
wires: [
|
||||
{
|
||||
lhs: {
|
||||
pin: "in",
|
||||
start: 0,
|
||||
end: 1,
|
||||
span: { start: 4, end: 12, line: 1 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "true",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 15, end: 19, line: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "in",
|
||||
start: 3,
|
||||
end: 5,
|
||||
span: { start: 29, end: 37, line: 2 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "six",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 40, end: 43, line: 2 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "in",
|
||||
start: 7,
|
||||
end: 7,
|
||||
span: { start: 53, end: 58, line: 3 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "true",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 61, end: 65, line: 3 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "out",
|
||||
start: 3,
|
||||
end: 7,
|
||||
span: { start: 75, end: 84, line: 4 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "out1",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 87, end: 91, line: 4 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "address",
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
span: { start: 101, end: 108, line: 5 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "address",
|
||||
start: 0,
|
||||
end: 13,
|
||||
span: { start: 109, end: 123, line: 5 },
|
||||
},
|
||||
},
|
||||
{
|
||||
lhs: {
|
||||
pin: "out",
|
||||
start: 2,
|
||||
end: 3,
|
||||
span: { start: 133, end: 142, line: 6 },
|
||||
},
|
||||
rhs: {
|
||||
pin: "address",
|
||||
start: 5,
|
||||
end: 6,
|
||||
span: { start: 143, end: 156, line: 6 },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pins", () => {
|
||||
it("parses a simple decl", () => {
|
||||
const decl = grammar.match("a", "PinDecl");
|
||||
expect(decl).toHaveSucceeded();
|
||||
expect(hdlSemantics(decl).PinDecl).toEqual({ pin: "a", width: 1 });
|
||||
});
|
||||
|
||||
it("parses a wide decl", () => {
|
||||
const decl = grammar.match("a[3]", "PinDecl");
|
||||
expect(decl).toHaveSucceeded();
|
||||
expect(hdlSemantics(decl).PinDecl).toEqual({ pin: "a", width: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("entire chips", () => {
|
||||
it("parses basic chip", () => {
|
||||
const match = grammar.match(AND_BUILTIN);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect<HdlParse>(hdlSemantics(match).Chip).toEqual({
|
||||
name: { value: "And", span: { start: 5, end: 8, line: 1 } },
|
||||
ins: [
|
||||
{ pin: "a", width: 1 },
|
||||
{ pin: "b", width: 1 },
|
||||
],
|
||||
outs: [{ pin: "out", width: 1 }],
|
||||
parts: "BUILTIN",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses chip with parts", () => {
|
||||
const match = grammar.match(NOT_PARTS);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect<HdlParse>(hdlSemantics(match).Chip).toEqual({
|
||||
name: { value: "Not", span: { start: 5, end: 8, line: 1 } },
|
||||
ins: [{ pin: "in", width: 1 }],
|
||||
outs: [{ pin: "out", width: 1 }],
|
||||
parts: [
|
||||
{
|
||||
name: "Nand",
|
||||
span: { start: 50, end: 76, line: 5 },
|
||||
wires: [
|
||||
{
|
||||
lhs: { pin: "a", span: { start: 55, end: 56, line: 5 } },
|
||||
rhs: { pin: "in", span: { start: 57, end: 59, line: 5 } },
|
||||
},
|
||||
{
|
||||
lhs: { pin: "b", span: { start: 61, end: 62, line: 5 } },
|
||||
rhs: { pin: "in", span: { start: 63, end: 65, line: 5 } },
|
||||
},
|
||||
{
|
||||
lhs: { pin: "out", span: { start: 67, end: 70, line: 5 } },
|
||||
rhs: { pin: "out", span: { start: 71, end: 74, line: 5 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses chip without parts", () => {
|
||||
const match = grammar.match(NOT_NO_PARTS);
|
||||
expect(match).toHaveSucceeded();
|
||||
|
||||
expect<HdlParse>(hdlSemantics(match).Chip).toEqual({
|
||||
name: { value: "Not", span: { start: 5, end: 8, line: 1 } },
|
||||
ins: [{ pin: "in", width: 1 }],
|
||||
outs: [{ pin: "out", width: 1 }],
|
||||
parts: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses chip using builtins", () => {
|
||||
const match = grammar.match(AND_16_BUILTIN);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect<HdlParse>(hdlSemantics(match).Chip).toEqual({
|
||||
name: { value: "And16", span: { start: 5, end: 10, line: 1 } },
|
||||
ins: [
|
||||
{ pin: "a", width: 16 },
|
||||
{ pin: "b", width: 16 },
|
||||
],
|
||||
outs: [{ pin: "out", width: 16 }],
|
||||
parts: "BUILTIN",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a chip with clocked pins", () => {
|
||||
const match = grammar.match(CLOCKED);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect<HdlParse>(hdlSemantics(match).Chip).toEqual({
|
||||
name: { value: "Foo", span: { start: 5, end: 8, line: 1 } },
|
||||
ins: [{ pin: "in", width: 1 }],
|
||||
outs: [],
|
||||
parts: [],
|
||||
clocked: ["in"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it.each(ERRORS)("fails with reasonable errors", (source, message) => {
|
||||
expect(grammar.match(source)).toHaveFailed(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/** Reads and parses HDL chip descriptions. */
|
||||
import { grammar as ohmGrammar } from "ohm-js";
|
||||
import { baseSemantics, grammars, makeParser, Span, span } from "./base.js";
|
||||
|
||||
export interface PinIndex {
|
||||
start?: number | undefined;
|
||||
end?: number | undefined;
|
||||
}
|
||||
|
||||
export interface PinParts extends PinIndex {
|
||||
pin: string;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface PinDeclaration {
|
||||
pin: string | string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface Wire {
|
||||
lhs: PinParts;
|
||||
rhs: PinParts;
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
name: string;
|
||||
wires: Wire[];
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface HdlParse {
|
||||
name: { value: string; span?: Span };
|
||||
ins: PinDeclaration[];
|
||||
outs: PinDeclaration[];
|
||||
clocked: string[];
|
||||
parts: "BUILTIN" | Part[];
|
||||
}
|
||||
|
||||
import hdlGrammar from "./grammars/hdl.ohm.js";
|
||||
export const grammar = ohmGrammar(hdlGrammar, grammars);
|
||||
|
||||
export const hdlSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
hdlSemantics.addAttribute<PinIndex>("SubBus", {
|
||||
SubBus(_a, startNode, endNode, _b) {
|
||||
const start = startNode.value;
|
||||
const end = endNode.child(0)?.child(1)?.value ?? start;
|
||||
return { start, end };
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<PinParts>("WireSide", {
|
||||
WireSide({ name }, index) {
|
||||
const { start, end } = (index.child(0)?.SubBus as PinIndex) ?? {
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
};
|
||||
return { pin: name, start, end, span: span(this.source) };
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<Wire>("Wire", {
|
||||
Wire(left, _, right) {
|
||||
const rhs: PinParts = right.isTerminal()
|
||||
? { pin: right.sourceString }
|
||||
: right.WireSide;
|
||||
return { lhs: left.WireSide as PinParts, rhs };
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<Wire[]>("Wires", {
|
||||
Wires(list) {
|
||||
return list.asIteration().children.map((node) => node.Wire as Wire);
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<Part>("Part", {
|
||||
Part({ name }, _a, { Wires }, _b, _c) {
|
||||
return {
|
||||
name: name as string,
|
||||
wires: Wires as Wire[],
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<Part[] | "BUILTIN">("Parts", {
|
||||
Parts(_, parts) {
|
||||
return parts.children.map((c) => c.Part);
|
||||
},
|
||||
BuiltinPart(_a, _b) {
|
||||
return "BUILTIN";
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<"BUILTIN" | Part[]>("PartList", {
|
||||
PartList(list) {
|
||||
return list.Parts;
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<string[]>("Clocked", {
|
||||
ClockedList(_a, clocked, _b) {
|
||||
return (
|
||||
clocked
|
||||
.asIteration()
|
||||
.children.map(
|
||||
({ sourceString }: { sourceString: string }) => sourceString,
|
||||
) ?? []
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<PinDeclaration>("PinDecl", {
|
||||
PinDecl({ name }, width) {
|
||||
return {
|
||||
pin: name,
|
||||
width: width.child(0)?.child(1)?.value ?? 1,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<PinDeclaration[]>("PinList", {
|
||||
PinList(list) {
|
||||
return list
|
||||
.asIteration()
|
||||
.children.map((node) => node.PinDecl as PinDeclaration);
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<HdlParse>("Chip", {
|
||||
Chip(_a, name, _b, body, _c) {
|
||||
return {
|
||||
name: { value: name.sourceString, span: span(name.source) },
|
||||
ins: body.child(0).child(0)?.child(1)?.PinList ?? [],
|
||||
outs: body.child(1).child(0)?.child(1)?.PinList ?? [],
|
||||
parts: body.child(2).PartList ?? [],
|
||||
clocked: body.child(3).child(0)?.Clocked,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
hdlSemantics.addAttribute<HdlParse>("Root", {
|
||||
Root(root) {
|
||||
return root.child(0)?.Chip;
|
||||
},
|
||||
});
|
||||
|
||||
export const HDL = {
|
||||
parser: grammar,
|
||||
grammar: hdlGrammar,
|
||||
semantics: hdlSemantics,
|
||||
parse: makeParser<HdlParse>(grammar, hdlSemantics, (n) => n.Chip),
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result";
|
||||
import { Programs } from "@nand2tetris/projects/samples/project_11/index.js";
|
||||
import { JACK } from "./jack";
|
||||
|
||||
describe("jack language", () => {
|
||||
describe.each(Object.keys(Programs))("%s", (program) => {
|
||||
it.each(Object.keys(Programs[program]))("%s", (filename) => {
|
||||
const parsed = JACK.parse(Programs[program][filename].jack);
|
||||
expect(parsed).toBeOk();
|
||||
expect(unwrap(parsed)).toEqual(Programs[program][filename].parsed);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,438 @@
|
||||
import { type Node, grammar as ohmGrammar } from "ohm-js";
|
||||
import { baseSemantics, grammars, makeParser, Span, span } from "./base.js";
|
||||
import jackGrammar from "./grammars/jack.ohm.js";
|
||||
|
||||
const primitives = new Set(["int", "boolean", "char"] as const);
|
||||
export type Primitive = typeof primitives extends Set<infer S> ? S : never;
|
||||
|
||||
export function isPrimitive(value: string): value is Primitive {
|
||||
return primitives.has(value as Primitive);
|
||||
}
|
||||
|
||||
export type Type = Primitive | string;
|
||||
|
||||
export interface Class {
|
||||
name: { value: string; span: Span };
|
||||
varDecs: ClassVarDec[];
|
||||
subroutines: Subroutine[];
|
||||
}
|
||||
|
||||
export type ClassVarType = "static" | "field";
|
||||
|
||||
export interface ClassVarDec {
|
||||
varType: ClassVarType;
|
||||
type: { value: Type; span: Span };
|
||||
names: string[];
|
||||
}
|
||||
|
||||
export interface Parameter {
|
||||
type: { value: Type; span: Span };
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ReturnType = Type | "void";
|
||||
export type SubroutineType = "constructor" | "function" | "method";
|
||||
|
||||
export interface Subroutine {
|
||||
type: SubroutineType;
|
||||
name: { value: string; span: Span };
|
||||
returnType: { value: ReturnType; span: Span };
|
||||
parameters: Parameter[];
|
||||
body: SubroutineBody;
|
||||
}
|
||||
|
||||
export interface SubroutineBody {
|
||||
varDecs: VarDec[];
|
||||
statements: Statement[];
|
||||
}
|
||||
|
||||
export interface VarDec {
|
||||
type: { value: Type; span: Span };
|
||||
names: string[];
|
||||
}
|
||||
|
||||
export type Statement =
|
||||
| LetStatement
|
||||
| IfStatement
|
||||
| WhileStatement
|
||||
| DoStatement
|
||||
| ReturnStatement;
|
||||
|
||||
export interface LetStatement {
|
||||
statementType: "letStatement";
|
||||
name: { value: string; span: Span };
|
||||
arrayIndex?: Expression;
|
||||
value: Expression;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface IfStatement {
|
||||
statementType: "ifStatement";
|
||||
condition: Expression;
|
||||
body: Statement[];
|
||||
else: Statement[];
|
||||
}
|
||||
|
||||
export interface WhileStatement {
|
||||
statementType: "whileStatement";
|
||||
condition: Expression;
|
||||
body: Statement[];
|
||||
}
|
||||
|
||||
export interface DoStatement {
|
||||
statementType: "doStatement";
|
||||
call: SubroutineCall;
|
||||
}
|
||||
|
||||
export interface ReturnStatement {
|
||||
statementType: "returnStatement";
|
||||
value?: Expression;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export type Op = "+" | "-" | "*" | "/" | "&" | "|" | "<" | ">" | "=";
|
||||
export type KeywordConstant = "true" | "false" | "null" | "this";
|
||||
export type UnaryOp = "-" | "~";
|
||||
|
||||
export type Term =
|
||||
| NumericLiteral
|
||||
| StringLiteral
|
||||
| Variable
|
||||
| KeywordLiteral
|
||||
| SubroutineCall
|
||||
| ArrayAccess
|
||||
| GroupedExpression
|
||||
| UnaryExpression;
|
||||
|
||||
export interface NumericLiteral {
|
||||
termType: "numericLiteral";
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface StringLiteral {
|
||||
termType: "stringLiteral";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface KeywordLiteral {
|
||||
termType: "keywordLiteral";
|
||||
value: KeywordConstant;
|
||||
}
|
||||
|
||||
export interface Variable {
|
||||
termType: "variable";
|
||||
name: string;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface GroupedExpression {
|
||||
termType: "groupedExpression";
|
||||
expression: Expression;
|
||||
}
|
||||
|
||||
export interface UnaryExpression {
|
||||
termType: "unaryExpression";
|
||||
op: UnaryOp;
|
||||
term: Term;
|
||||
}
|
||||
|
||||
export interface ArrayAccess {
|
||||
termType: "arrayAccess";
|
||||
name: { value: string; span: Span };
|
||||
index: Expression;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface SubroutineCall {
|
||||
termType: "subroutineCall";
|
||||
name: { value: string; span: Span };
|
||||
span: Span;
|
||||
parameters: Expression[];
|
||||
}
|
||||
|
||||
export interface ExpressionPart {
|
||||
op: Op;
|
||||
term: Term;
|
||||
}
|
||||
|
||||
export interface Expression {
|
||||
term: Term;
|
||||
rest: ExpressionPart[];
|
||||
}
|
||||
|
||||
export const grammar = ohmGrammar(jackGrammar, grammars);
|
||||
export const jackSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
function statements(node: Node) {
|
||||
return node.children.map((n) => n.statement);
|
||||
}
|
||||
|
||||
jackSemantics.addAttribute<Class>("Root", {
|
||||
Root(_) {
|
||||
return this.class;
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Class>("class", {
|
||||
Class(_a, name, _b, varDecs, subroutines, _c) {
|
||||
return {
|
||||
name: { value: name.sourceString, span: span(name.source) },
|
||||
varDecs: varDecs.children.map((n) => n.classVarDec),
|
||||
subroutines: subroutines.children.map((n) => n.subroutineDec),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<ClassVarDec>("classVarDec", {
|
||||
ClassVarDec(varType, type, name, rest, _) {
|
||||
return {
|
||||
varType: varType.sourceString.trim() as ClassVarType,
|
||||
type: {
|
||||
value: type.sourceString.trim() as Type,
|
||||
span: span(type.source),
|
||||
},
|
||||
names: [
|
||||
name.sourceString,
|
||||
...rest.children.map((n) => n.child(1).sourceString),
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Subroutine>("subroutineDec", {
|
||||
SubroutineDec(type, returnType, name, _a, parameters, _b, body) {
|
||||
return {
|
||||
type: type.sourceString.trim() as SubroutineType,
|
||||
returnType: {
|
||||
value: returnType.sourceString.trim() as ReturnType,
|
||||
span: span(returnType.source),
|
||||
},
|
||||
name: { value: name.sourceString, span: span(name.source) },
|
||||
parameters: parameters.parameterList,
|
||||
body: body.subroutineBody,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Parameter>("parameter", {
|
||||
Parameter(type, name) {
|
||||
return {
|
||||
type: {
|
||||
value: type.sourceString.trim() as Type,
|
||||
span: span(type.source),
|
||||
},
|
||||
name: name.sourceString,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Expression[]>("parameterList", {
|
||||
ParameterList(node) {
|
||||
return node.child(0)?.parameters ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Expression[]>("parameters", {
|
||||
Parameters(first, rest) {
|
||||
return [first.parameter, ...rest.children.map((n) => n.child(1).parameter)];
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<SubroutineBody>("subroutineBody", {
|
||||
SubroutineBody(_a, varDecs, statementList, _b) {
|
||||
return {
|
||||
varDecs: varDecs.children.map((n) => n.varDec),
|
||||
statements: statements(statementList),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<VarDec>("varDec", {
|
||||
VarDec(_a, type, name, rest, _b) {
|
||||
return {
|
||||
type: {
|
||||
value: type.sourceString.trim() as Type,
|
||||
span: span(type.source),
|
||||
},
|
||||
names: [
|
||||
name.sourceString,
|
||||
...rest.children.map((n) => n.child(1).sourceString),
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// jackSemantics.addAttribute<string | ArrayAccess>("letTarget", {
|
||||
// LetTarget() {
|
||||
// jackI
|
||||
// }
|
||||
// })
|
||||
|
||||
jackSemantics.addAttribute<Statement>("statement", {
|
||||
LetStatement(_a, target, _b, value, _c) {
|
||||
if (target.term.termType == "variable") {
|
||||
return {
|
||||
statementType: "letStatement",
|
||||
name: {
|
||||
value: (target.term as Variable).name,
|
||||
span: (target.term as Variable).span,
|
||||
},
|
||||
value: value.expression,
|
||||
span: span(this.source),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statementType: "letStatement",
|
||||
name: (target.term as ArrayAccess).name,
|
||||
arrayIndex: (target.term as ArrayAccess).index,
|
||||
value: value.expression,
|
||||
span: span(this.source),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
IfStatement(_a, _b, condition, _c, _d, body, _e, elseBlock) {
|
||||
return {
|
||||
statementType: "ifStatement",
|
||||
condition: condition.expression,
|
||||
body: statements(body),
|
||||
else: elseBlock.child(0)?.else ?? [],
|
||||
};
|
||||
},
|
||||
|
||||
WhileStatement(_a, _b, condition, _c, _d, body, _e) {
|
||||
return {
|
||||
statementType: "whileStatement",
|
||||
condition: condition.expression,
|
||||
body: statements(body),
|
||||
};
|
||||
},
|
||||
|
||||
DoStatement(_a, call, _b) {
|
||||
return { statementType: "doStatement", call: call.term as SubroutineCall };
|
||||
},
|
||||
|
||||
EmptyReturn(_a, _b) {
|
||||
return {
|
||||
statementType: "returnStatement",
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
|
||||
ReturnValue(_a, value, _b) {
|
||||
return {
|
||||
statementType: "returnStatement",
|
||||
value: value.expression,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Statement[]>("else", {
|
||||
ElseBlock(_a, _b, body, _c) {
|
||||
return statements(body);
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Term>("term", {
|
||||
integerConstant(node) {
|
||||
return {
|
||||
termType: "numericLiteral",
|
||||
value: Number(node.sourceString),
|
||||
};
|
||||
},
|
||||
|
||||
stringConstant(_a, _b, _c) {
|
||||
return { termType: "stringLiteral", value: this.sourceString.slice(1, -1) };
|
||||
},
|
||||
|
||||
keywordConstant(_) {
|
||||
return {
|
||||
termType: "keywordLiteral",
|
||||
value: this.sourceString as KeywordConstant,
|
||||
};
|
||||
},
|
||||
|
||||
SubroutineCall(name, _a, expressions, _b) {
|
||||
return {
|
||||
termType: "subroutineCall",
|
||||
name: { value: name.sourceString, span: span(name.source) },
|
||||
parameters: expressions.expressionList,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
|
||||
ArrayAccess(start, index, _) {
|
||||
const name = start.child(0);
|
||||
return {
|
||||
termType: "arrayAccess",
|
||||
name: { value: name.sourceString, span: span(name.source) },
|
||||
index: index.expression,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
|
||||
jackIdentifier(first, rest) {
|
||||
return {
|
||||
termType: "variable",
|
||||
name: `${first.sourceString}${rest.sourceString}`,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
|
||||
GroupedExpression(_a, expression, _b) {
|
||||
return {
|
||||
termType: "groupedExpression",
|
||||
expression: expression.expression,
|
||||
};
|
||||
},
|
||||
|
||||
UnaryExpression(op, term) {
|
||||
return {
|
||||
termType: "unaryExpression",
|
||||
op: op.sourceString as UnaryOp,
|
||||
term: term.term,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Expression[]>("expressionList", {
|
||||
ExpressionList(node) {
|
||||
return node.child(0)?.expressions ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Expression[]>("expressions", {
|
||||
Expressions(first, rest) {
|
||||
return [
|
||||
first.expression,
|
||||
...rest.children.map((n) => n.child(1).expression),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<Expression>("expression", {
|
||||
Expression(first, rest) {
|
||||
return {
|
||||
nodeType: "expression",
|
||||
term: first.term,
|
||||
rest: rest.children.map((n) => n.expressionPart),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
jackSemantics.addAttribute<ExpressionPart>("expressionPart", {
|
||||
ExpressionPart(op, term) {
|
||||
return {
|
||||
op: op.sourceString as Op,
|
||||
term: term.term,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const JACK = {
|
||||
parser: grammar,
|
||||
grammar: jackGrammar,
|
||||
semantics: jackSemantics,
|
||||
parse: makeParser<Class>(grammar, jackSemantics, (n) => n.class),
|
||||
};
|
||||
@@ -0,0 +1,507 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { resetFiles } from "@nand2tetris/projects/full.js";
|
||||
import { grammar, TST } from "./tst.js";
|
||||
|
||||
const NOT_TST = `
|
||||
output-list in%B3.1.3 out%B3.1.3;
|
||||
|
||||
set in 0, eval, output;
|
||||
set in 1, eval, output;`;
|
||||
|
||||
const BIT_TST = `
|
||||
output-list time%S1.4.1 in%B2.1.2 load%B2.1.2 out%B2.1.2;
|
||||
set in 0, set load 0, tick, output; tock, output;
|
||||
set in 0, set load 1, eval, output;
|
||||
`;
|
||||
|
||||
const MEM_TST = `
|
||||
output-list time%S1.2.1 in%B2.1.2;
|
||||
set in -32123, tick, output;
|
||||
`;
|
||||
|
||||
const MEM_REPEAT = `
|
||||
repeat 14 {
|
||||
eval, output;
|
||||
}
|
||||
`;
|
||||
|
||||
const INDEF_REPEAT = `
|
||||
repeat {
|
||||
eval, output;
|
||||
}
|
||||
`;
|
||||
|
||||
const COND_WHILE = `while out <> 89 {
|
||||
eval;
|
||||
}`;
|
||||
|
||||
describe("tst language", () => {
|
||||
it("parses an output format", () => {
|
||||
const match = grammar.match("a%B3.1.3", "OutputFormat");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).format).toStrictEqual({
|
||||
id: "a",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: {
|
||||
style: "B",
|
||||
width: 1,
|
||||
lpad: 3,
|
||||
rpad: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an output list", () => {
|
||||
const match = grammar.match(
|
||||
"output-list a%B1.1.1 out%X2.3.4",
|
||||
"TstOutputListOperation",
|
||||
);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).operation).toStrictEqual({
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "a",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 1, rpad: 1 },
|
||||
},
|
||||
{
|
||||
id: "out",
|
||||
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "X", width: 3, lpad: 2, rpad: 4 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an output list with junk", () => {
|
||||
const match = grammar.match(
|
||||
"\n/// A list\noutput-list a%B1.1.1 /* the output */ out%X2.3.4",
|
||||
"TstOutputListOperation",
|
||||
);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).operation).toStrictEqual({
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "a",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 1, rpad: 1 },
|
||||
},
|
||||
{
|
||||
id: "out",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "X", width: 3, lpad: 2, rpad: 4 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an output list with builtins", () => {
|
||||
const match = grammar.match(
|
||||
"output-list PC[]%D0.4.0 RAM16K[0]%D1.7.1",
|
||||
"TstOutputListOperation",
|
||||
);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).operation).toStrictEqual({
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "PC",
|
||||
builtin: true,
|
||||
address: -1,
|
||||
format: { style: "D", width: 4, lpad: 0, rpad: 0 },
|
||||
},
|
||||
{
|
||||
id: "RAM16K",
|
||||
builtin: true,
|
||||
address: 0,
|
||||
format: { style: "D", width: 7, lpad: 1, rpad: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses file ops", () => {
|
||||
const match = grammar.match(
|
||||
"load A.hdl, output-file A.out, compare-to A.cmp, output-list a%B1.1.1;",
|
||||
);
|
||||
expect(match).toHaveSucceeded();
|
||||
});
|
||||
|
||||
it("parses a single set", () => {
|
||||
const match = grammar.match("set a 0", "TstSetOperation");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).operation).toEqual({
|
||||
op: "set",
|
||||
id: "a",
|
||||
value: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses simple multiline", () => {
|
||||
const match = grammar.match("eval;\n\neval;\n\n");
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ";",
|
||||
span: { start: 0, end: 5, line: 1 },
|
||||
},
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ";",
|
||||
span: { start: 7, end: 12, line: 3 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a test file", () => {
|
||||
const match = grammar.match(NOT_TST);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
op: {
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "in",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 3, rpad: 3 },
|
||||
},
|
||||
{
|
||||
id: "out",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 3, rpad: 3 },
|
||||
},
|
||||
],
|
||||
},
|
||||
separator: ";",
|
||||
span: { line: 2, start: 1, end: 34 },
|
||||
},
|
||||
|
||||
{
|
||||
op: { op: "set", id: "in", value: 0 },
|
||||
separator: ",",
|
||||
span: { line: 4, start: 36, end: 45 },
|
||||
},
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ",",
|
||||
span: { line: 4, start: 46, end: 51 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 4, start: 52, end: 59 },
|
||||
},
|
||||
|
||||
{
|
||||
op: { op: "set", id: "in", value: 1 },
|
||||
separator: ",",
|
||||
span: { line: 5, start: 60, end: 69 },
|
||||
},
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ",",
|
||||
span: { line: 5, start: 70, end: 75 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 5, start: 76, end: 83 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a clocked test file", () => {
|
||||
const match = grammar.match(BIT_TST);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
op: {
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "time",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "S", width: 4, lpad: 1, rpad: 1 },
|
||||
},
|
||||
{
|
||||
id: "in",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 2, rpad: 2 },
|
||||
},
|
||||
{
|
||||
id: "load",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 2, rpad: 2 },
|
||||
},
|
||||
{
|
||||
id: "out",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 2, rpad: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
separator: ";",
|
||||
span: { line: 2, start: 1, end: 58 },
|
||||
},
|
||||
|
||||
{
|
||||
op: { op: "set", id: "in", value: 0 },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 59, end: 68 },
|
||||
},
|
||||
{
|
||||
op: { op: "set", id: "load", value: 0 },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 69, end: 80 },
|
||||
},
|
||||
{
|
||||
op: { op: "tick" },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 81, end: 86 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 3, start: 87, end: 94 },
|
||||
},
|
||||
|
||||
{
|
||||
op: { op: "tock" },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 95, end: 100 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 3, start: 101, end: 108 },
|
||||
},
|
||||
|
||||
{
|
||||
op: { op: "set", id: "in", value: 0 },
|
||||
separator: ",",
|
||||
span: { line: 4, start: 109, end: 118 },
|
||||
},
|
||||
{
|
||||
op: { op: "set", id: "load", value: 1 },
|
||||
separator: ",",
|
||||
span: { line: 4, start: 119, end: 130 },
|
||||
},
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ",",
|
||||
span: { line: 4, start: 131, end: 136 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 4, start: 137, end: 144 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a test file with negative integers", () => {
|
||||
const match = grammar.match(MEM_TST);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
// output-list time%S1.2.1 in%B2.1.2;
|
||||
{
|
||||
op: {
|
||||
op: "output-list",
|
||||
spec: [
|
||||
{
|
||||
id: "time",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "S", width: 2, lpad: 1, rpad: 1 },
|
||||
},
|
||||
{
|
||||
id: "in",
|
||||
builtin: false,
|
||||
address: -1,
|
||||
format: { style: "B", width: 1, lpad: 2, rpad: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
separator: ";",
|
||||
span: {
|
||||
start: 1,
|
||||
end: 35,
|
||||
line: 2,
|
||||
},
|
||||
},
|
||||
// set in -32123, tick, output;
|
||||
{
|
||||
op: { op: "set", id: "in", value: 33413 /* unsigned */ },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 36, end: 50 },
|
||||
},
|
||||
{
|
||||
op: { op: "tick" },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 51, end: 56 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 3, start: 57, end: 64 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("repeats blocks", () => {
|
||||
const match = grammar.match(MEM_REPEAT);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
count: 14,
|
||||
statements: [
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 15, end: 20 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 3, start: 21, end: 28 },
|
||||
},
|
||||
],
|
||||
span: {
|
||||
start: 1,
|
||||
end: 30,
|
||||
line: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("repeats indefinitely", () => {
|
||||
const match = grammar.match(INDEF_REPEAT);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
count: -1,
|
||||
span: {
|
||||
start: 1,
|
||||
end: 27,
|
||||
line: 2,
|
||||
},
|
||||
statements: [
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ",",
|
||||
span: { line: 3, start: 12, end: 17 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { line: 3, start: 18, end: 25 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("loops with a condition", () => {
|
||||
const match = grammar.match(COND_WHILE);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
span: {
|
||||
start: 0,
|
||||
end: 27,
|
||||
line: 1,
|
||||
},
|
||||
condition: {
|
||||
op: "<>",
|
||||
left: "out",
|
||||
right: 89,
|
||||
},
|
||||
statements: [
|
||||
{
|
||||
op: { op: "eval" },
|
||||
separator: ";",
|
||||
span: {
|
||||
start: 20,
|
||||
end: 25,
|
||||
line: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("loads ROMs", () => {
|
||||
const match = grammar.match(`ROM32K load Max.hack;`);
|
||||
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(TST.semantics(match).tst).toEqual({
|
||||
lines: [
|
||||
{
|
||||
span: {
|
||||
start: 0,
|
||||
end: 21,
|
||||
line: 1,
|
||||
},
|
||||
op: { op: "loadRom", file: "Max.hack" },
|
||||
separator: ";",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("loads all project tst files", async () => {
|
||||
const fs = new FileSystem(new ObjectFileSystemAdapter({}));
|
||||
await resetFiles(fs);
|
||||
async function check() {
|
||||
for (const stat of await fs.scandir(".")) {
|
||||
if (stat.isDirectory()) {
|
||||
fs.pushd(stat.name);
|
||||
await check();
|
||||
fs.popd();
|
||||
} else {
|
||||
if (stat.name.endsWith("vm_tst")) {
|
||||
const tst = await fs.readFile(stat.name);
|
||||
const match = grammar.match(tst);
|
||||
expect(match).toHaveSucceeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await check();
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
/** Reads tst files to apply and perform test runs. */
|
||||
|
||||
import { grammar as ohmGrammar } from "ohm-js";
|
||||
import { baseSemantics, grammars, makeParser, Span, span } from "./base.js";
|
||||
|
||||
export interface TstEchoOperation {
|
||||
op: "echo";
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TstClearEchoOperation {
|
||||
op: "clear-echo";
|
||||
}
|
||||
|
||||
export interface TstSetOperation {
|
||||
op: "set";
|
||||
id: string;
|
||||
index?: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TstEvalOperation {
|
||||
op: "eval" | "tick" | "tock" | "ticktock" | "vmstep";
|
||||
}
|
||||
|
||||
export interface TstOutputOperation {
|
||||
op: "output";
|
||||
}
|
||||
|
||||
export interface TstOutputFormat {
|
||||
style: "D" | "X" | "B" | "S";
|
||||
width: number;
|
||||
lpad: number;
|
||||
rpad: number;
|
||||
}
|
||||
|
||||
export interface TstOutputSpec {
|
||||
id: string;
|
||||
builtin: boolean;
|
||||
address: number;
|
||||
format?: TstOutputFormat;
|
||||
}
|
||||
|
||||
export interface TstOutputListOperation {
|
||||
op: "output-list";
|
||||
spec: TstOutputSpec[];
|
||||
}
|
||||
|
||||
export interface TstLoadROMOperation {
|
||||
op: "loadRom";
|
||||
file: string;
|
||||
}
|
||||
|
||||
export interface TstFileOperation {
|
||||
op: "load" | "output-file" | "compare-to";
|
||||
file?: string;
|
||||
}
|
||||
|
||||
export interface TstResetRamOperation {
|
||||
op: "resetRam";
|
||||
}
|
||||
|
||||
export type TstOperation =
|
||||
| TstFileOperation
|
||||
| TstEvalOperation
|
||||
| TstEchoOperation
|
||||
| TstClearEchoOperation
|
||||
| TstOutputOperation
|
||||
| TstSetOperation
|
||||
| TstOutputListOperation
|
||||
| TstLoadROMOperation
|
||||
| TstResetRamOperation;
|
||||
|
||||
export type Separator = "," | ";" | "!";
|
||||
|
||||
export interface TstCommand {
|
||||
op: TstOperation;
|
||||
separator: Separator;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface TstRepeat {
|
||||
statements: TstCommand[];
|
||||
count: number;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export interface TstWhileCondition {
|
||||
op: "<" | "<=" | "=" | ">=" | ">" | "<>";
|
||||
left: string | number;
|
||||
right: string | number;
|
||||
}
|
||||
|
||||
export interface TstWhileStatement {
|
||||
statements: TstCommand[];
|
||||
condition: TstWhileCondition;
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export type TstStatement = TstCommand | TstRepeat | TstWhileStatement;
|
||||
|
||||
export interface Tst {
|
||||
lines: TstStatement[];
|
||||
}
|
||||
|
||||
import tstGrammar from "./grammars/tst.ohm.js";
|
||||
export const grammar = ohmGrammar(tstGrammar, grammars);
|
||||
export const tstSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
tstSemantics.extendAttribute<number>("value", {
|
||||
Index(_a, idx, _b) {
|
||||
return idx?.child(0)?.value ?? -1;
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.extendAttribute<string>("name", {
|
||||
FileName({ name }) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<number>("index", {
|
||||
Index(_open, dec, _close) {
|
||||
return dec.child(0)?.value ?? 0;
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstOutputFormat>("formatSpec", {
|
||||
FormatSpec(
|
||||
_a,
|
||||
{ sourceString: style },
|
||||
{ value: lpad },
|
||||
_b,
|
||||
{ value: width },
|
||||
_c,
|
||||
{ value: rpad },
|
||||
) {
|
||||
return {
|
||||
style: style as TstOutputFormat["style"],
|
||||
width,
|
||||
lpad,
|
||||
rpad,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstOutputSpec>("format", {
|
||||
OutputFormat({ name: id }, index, formatSpec) {
|
||||
return {
|
||||
id,
|
||||
builtin: index?.child(0) !== undefined,
|
||||
address: index?.child(0)?.value ?? -1,
|
||||
format: formatSpec?.child(0)?.formatSpec,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstOperation>("operation", {
|
||||
TstEvalOperation(op) {
|
||||
return { op: op.sourceString as TstEvalOperation["op"] };
|
||||
},
|
||||
TstOutputOperation(_) {
|
||||
return { op: "output" };
|
||||
},
|
||||
TstOutputListOperation(_, formats) {
|
||||
return {
|
||||
op: "output-list",
|
||||
spec: formats.children.map((n) => n.format),
|
||||
};
|
||||
},
|
||||
TstSetOperation(op, { name }, index, { value }) {
|
||||
const setOp: TstSetOperation = {
|
||||
op: "set",
|
||||
id: name,
|
||||
value,
|
||||
};
|
||||
const child = index.child(0)?.child(1)?.child(0);
|
||||
if (child) {
|
||||
setOp.index = child.value;
|
||||
}
|
||||
return setOp;
|
||||
},
|
||||
TstEchoOperation(op, str) {
|
||||
return {
|
||||
op: "echo",
|
||||
message: str.String as string,
|
||||
};
|
||||
},
|
||||
TstClearEchoOperation(op) {
|
||||
return {
|
||||
op: "clear-echo",
|
||||
};
|
||||
},
|
||||
TstLoadROMOperation(_r, _l, name) {
|
||||
return {
|
||||
op: "loadRom",
|
||||
file: name.sourceString,
|
||||
};
|
||||
},
|
||||
TstFileOperation(op, file) {
|
||||
return {
|
||||
op: op.sourceString as TstFileOperation["op"],
|
||||
file: file?.sourceString,
|
||||
};
|
||||
},
|
||||
TstResetRAMOperation(_) {
|
||||
return {
|
||||
op: "resetRam",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstCommand>("command", {
|
||||
TstCommand(op, sep) {
|
||||
return {
|
||||
op: op.operation,
|
||||
separator: sep.sourceString as Separator,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstWhileCondition>("condition", {
|
||||
Condition({ value: left }, { sourceString: op }, { value: right }) {
|
||||
return {
|
||||
left,
|
||||
right,
|
||||
op: op as "<" | "<=" | "=" | ">=" | ">" | "<>",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<TstStatement>("statement", {
|
||||
TstWhile(op, cond, _o, commands, _c) {
|
||||
return {
|
||||
statements: commands.children.map((node) => node.command as TstCommand),
|
||||
condition: cond.condition,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
TstRepeat(op, count, _o, commands, _c) {
|
||||
return {
|
||||
statements: commands.children.map((node) => node.command as TstCommand),
|
||||
count: count.sourceString ? Number(count.sourceString) : -1,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
TstStatement(command) {
|
||||
return command.command;
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<Tst>("tst", {
|
||||
Tst(lines) {
|
||||
return {
|
||||
lines: lines.children.map((n) => n.statement),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
tstSemantics.addAttribute<Tst>("root", {
|
||||
Root({ tst }) {
|
||||
return tst;
|
||||
},
|
||||
});
|
||||
|
||||
export const TST = {
|
||||
grammar: tstGrammar,
|
||||
semantics: tstSemantics,
|
||||
parser: grammar,
|
||||
parse: makeParser<Tst>(grammar, tstSemantics),
|
||||
};
|
||||
@@ -0,0 +1,495 @@
|
||||
import { grammar, VM, Vm } from "./vm.js";
|
||||
|
||||
const SIMPLE_ADD = `
|
||||
push constant 7
|
||||
push constant 8
|
||||
add`;
|
||||
|
||||
const SIMPLE_ADD_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 7,
|
||||
span: { start: 1, end: 16, line: 2 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 8,
|
||||
span: { start: 17, end: 32, line: 3 },
|
||||
},
|
||||
{ op: "add", span: { start: 33, end: 36, line: 4 } },
|
||||
],
|
||||
} satisfies Vm;
|
||||
|
||||
// d = (2 - x) + (y + 9)
|
||||
const FIG7_3A = `
|
||||
push constant 2
|
||||
push local 0
|
||||
sub
|
||||
push local 1
|
||||
push constant 9
|
||||
add
|
||||
add
|
||||
pop local 2
|
||||
`;
|
||||
|
||||
const FIG7_3A_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 2,
|
||||
span: { start: 1, end: 16, line: 2 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 17, end: 29, line: 3 },
|
||||
},
|
||||
{ op: "sub", span: { start: 30, end: 33, line: 4 } },
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 34, end: 46, line: 5 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 9,
|
||||
span: { start: 47, end: 62, line: 6 },
|
||||
},
|
||||
{ op: "add", span: { start: 63, end: 66, line: 7 } },
|
||||
{ op: "add", span: { start: 67, end: 70, line: 8 } },
|
||||
{
|
||||
op: "pop",
|
||||
segment: "local",
|
||||
offset: 2,
|
||||
span: { start: 71, end: 82, line: 9 },
|
||||
},
|
||||
],
|
||||
} satisfies Vm;
|
||||
|
||||
// (x < 7) or (y == 8)
|
||||
const FIG7_3B = `
|
||||
push local 0
|
||||
push constant 7
|
||||
lt
|
||||
push local 1
|
||||
push constant 8
|
||||
eq
|
||||
or
|
||||
`;
|
||||
const FIG7_3B_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 1, end: 13, line: 2 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 7,
|
||||
span: { start: 14, end: 29, line: 3 },
|
||||
},
|
||||
{ op: "lt", span: { start: 30, end: 32, line: 4 } },
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 33, end: 45, line: 5 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 8,
|
||||
span: { start: 46, end: 61, line: 6 },
|
||||
},
|
||||
{ op: "eq", span: { start: 62, end: 64, line: 7 } },
|
||||
{ op: "or", span: { start: 65, end: 67, line: 8 } },
|
||||
],
|
||||
} satisfies Vm;
|
||||
|
||||
const FIG8_1 = `
|
||||
// returns x * y
|
||||
// x = arg 0
|
||||
// y = arg 1
|
||||
// sum = local 0
|
||||
// i = local 1
|
||||
function mult 2
|
||||
push constant 0
|
||||
pop local 0
|
||||
push constant 0
|
||||
pop local 1
|
||||
label WHILE_LOOP
|
||||
push local 1
|
||||
push argument 1
|
||||
lt
|
||||
neg
|
||||
if-goto WHILE_END
|
||||
push local 0
|
||||
push argument 0
|
||||
add
|
||||
pop local 0
|
||||
push local 1
|
||||
push constant 1
|
||||
add
|
||||
pop local 1
|
||||
goto WHILE_LOOP
|
||||
label WHILE_END
|
||||
push local 0
|
||||
return
|
||||
`;
|
||||
const FIG8_1_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "function",
|
||||
name: "mult",
|
||||
nVars: 2,
|
||||
span: { start: 76, end: 91, line: 7 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 0,
|
||||
span: { start: 94, end: 109, line: 8 },
|
||||
},
|
||||
{
|
||||
op: "pop",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 112, end: 123, line: 9 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 0,
|
||||
span: { start: 126, end: 141, line: 10 },
|
||||
},
|
||||
{
|
||||
op: "pop",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 144, end: 155, line: 11 },
|
||||
},
|
||||
{
|
||||
op: "label",
|
||||
label: "WHILE_LOOP",
|
||||
span: { start: 156, end: 172, line: 12 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 175, end: 187, line: 13 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 1,
|
||||
span: { start: 190, end: 205, line: 14 },
|
||||
},
|
||||
{ op: "lt", span: { start: 208, end: 210, line: 15 } },
|
||||
{ op: "neg", span: { start: 213, end: 216, line: 16 } },
|
||||
{
|
||||
op: "if-goto",
|
||||
label: "WHILE_END",
|
||||
span: { start: 219, end: 236, line: 17 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 239, end: 251, line: 18 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 254, end: 269, line: 19 },
|
||||
},
|
||||
{ op: "add", span: { start: 272, end: 275, line: 20 } },
|
||||
{
|
||||
op: "pop",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 278, end: 289, line: 21 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 292, end: 304, line: 22 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 1,
|
||||
span: { start: 307, end: 322, line: 23 },
|
||||
},
|
||||
{ op: "add", span: { start: 325, end: 328, line: 24 } },
|
||||
{
|
||||
op: "pop",
|
||||
segment: "local",
|
||||
offset: 1,
|
||||
span: { start: 331, end: 342, line: 25 },
|
||||
},
|
||||
{
|
||||
op: "goto",
|
||||
label: "WHILE_LOOP",
|
||||
span: { start: 345, end: 360, line: 26 },
|
||||
},
|
||||
{
|
||||
op: "label",
|
||||
label: "WHILE_END",
|
||||
span: { start: 361, end: 376, line: 27 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "local",
|
||||
offset: 0,
|
||||
span: { start: 379, end: 391, line: 28 },
|
||||
},
|
||||
{ op: "return", span: { start: 394, end: 400, line: 29 } },
|
||||
],
|
||||
};
|
||||
|
||||
const FIG8_2 = `
|
||||
function main 0
|
||||
push constant 3
|
||||
push constant 4
|
||||
call hypot 2
|
||||
return
|
||||
|
||||
function hypot 2
|
||||
push argument 0
|
||||
push argument 0
|
||||
call mult 2
|
||||
push argument 1
|
||||
push argument 1
|
||||
call mult 2
|
||||
add
|
||||
call sqrt 1
|
||||
return
|
||||
`;
|
||||
const FIG8_2_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "function",
|
||||
name: "main",
|
||||
nVars: 0,
|
||||
span: { start: 1, end: 16, line: 2 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 3,
|
||||
span: { start: 19, end: 34, line: 3 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 4,
|
||||
span: { start: 37, end: 52, line: 4 },
|
||||
},
|
||||
{
|
||||
op: "call",
|
||||
name: "hypot",
|
||||
nArgs: 2,
|
||||
span: { start: 55, end: 67, line: 5 },
|
||||
},
|
||||
{ op: "return", span: { start: 70, end: 76, line: 6 } },
|
||||
{
|
||||
op: "function",
|
||||
name: "hypot",
|
||||
nVars: 2,
|
||||
span: { start: 78, end: 94, line: 8 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 97, end: 112, line: 9 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 115, end: 130, line: 10 },
|
||||
},
|
||||
{
|
||||
op: "call",
|
||||
name: "mult",
|
||||
nArgs: 2,
|
||||
span: { start: 133, end: 144, line: 11 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 1,
|
||||
span: { start: 147, end: 162, line: 12 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 1,
|
||||
span: { start: 165, end: 180, line: 13 },
|
||||
},
|
||||
{
|
||||
op: "call",
|
||||
name: "mult",
|
||||
nArgs: 2,
|
||||
span: { start: 183, end: 194, line: 14 },
|
||||
},
|
||||
{ op: "add", span: { start: 197, end: 200, line: 15 } },
|
||||
{
|
||||
op: "call",
|
||||
name: "sqrt",
|
||||
nArgs: 1,
|
||||
span: { start: 203, end: 214, line: 16 },
|
||||
},
|
||||
{ op: "return", span: { start: 217, end: 223, line: 17 } },
|
||||
],
|
||||
} satisfies Vm;
|
||||
|
||||
const FIG8_4 = `
|
||||
function main 0
|
||||
push constant 3
|
||||
call factorial 1
|
||||
return
|
||||
function factorial 1
|
||||
push argument 0
|
||||
push constant 1
|
||||
eq
|
||||
if-goto BASE_CASE
|
||||
push argument 0
|
||||
push argument 0
|
||||
push constant 1
|
||||
sub
|
||||
call factorial 1
|
||||
call mult 2
|
||||
return
|
||||
label BASE_CASE
|
||||
push constant 1
|
||||
return
|
||||
`;
|
||||
const FIG8_4_PARSED = {
|
||||
instructions: [
|
||||
{
|
||||
op: "function",
|
||||
name: "main",
|
||||
nVars: 0,
|
||||
span: { start: 1, end: 16, line: 2 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 3,
|
||||
span: { start: 17, end: 32, line: 3 },
|
||||
},
|
||||
{
|
||||
op: "call",
|
||||
name: "factorial",
|
||||
nArgs: 1,
|
||||
span: { start: 33, end: 49, line: 4 },
|
||||
},
|
||||
{ op: "return", span: { start: 50, end: 56, line: 5 } },
|
||||
{
|
||||
op: "function",
|
||||
name: "factorial",
|
||||
nVars: 1,
|
||||
span: { start: 57, end: 77, line: 6 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 78, end: 93, line: 7 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 1,
|
||||
span: { start: 94, end: 109, line: 8 },
|
||||
},
|
||||
{ op: "eq", span: { start: 110, end: 112, line: 9 } },
|
||||
{
|
||||
op: "if-goto",
|
||||
label: "BASE_CASE",
|
||||
span: { start: 113, end: 130, line: 10 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 131, end: 146, line: 11 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "argument",
|
||||
offset: 0,
|
||||
span: { start: 147, end: 162, line: 12 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 1,
|
||||
span: { start: 163, end: 178, line: 13 },
|
||||
},
|
||||
{ op: "sub", span: { start: 179, end: 182, line: 14 } },
|
||||
{
|
||||
op: "call",
|
||||
name: "factorial",
|
||||
nArgs: 1,
|
||||
span: { start: 183, end: 199, line: 15 },
|
||||
},
|
||||
{
|
||||
op: "call",
|
||||
name: "mult",
|
||||
nArgs: 2,
|
||||
span: { start: 200, end: 211, line: 16 },
|
||||
},
|
||||
{ op: "return", span: { start: 212, end: 218, line: 17 } },
|
||||
{
|
||||
op: "label",
|
||||
label: "BASE_CASE",
|
||||
span: { start: 219, end: 234, line: 18 },
|
||||
},
|
||||
{
|
||||
op: "push",
|
||||
segment: "constant",
|
||||
offset: 1,
|
||||
span: { start: 235, end: 250, line: 19 },
|
||||
},
|
||||
{ op: "return", span: { start: 251, end: 257, line: 20 } },
|
||||
],
|
||||
} satisfies Vm;
|
||||
|
||||
test.each([
|
||||
["Simple Add", SIMPLE_ADD, SIMPLE_ADD_PARSED],
|
||||
["Figure 7.3a", FIG7_3A, FIG7_3A_PARSED],
|
||||
["Figure 7.3b", FIG7_3B, FIG7_3B_PARSED],
|
||||
["Figure 8.1", FIG8_1, FIG8_1_PARSED],
|
||||
["Figure 8.2", FIG8_2, FIG8_2_PARSED],
|
||||
["Figure 8.4", FIG8_4, FIG8_4_PARSED],
|
||||
])("VM Parser: %s", (_name, fig, parsed) => {
|
||||
const match = grammar.match(fig);
|
||||
expect(match).toHaveSucceeded();
|
||||
expect(VM.semantics(match).vm).toStrictEqual(parsed);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["call mult", 'Line 1, col 10: expected "%B", ".", a digit, or "%X"'],
|
||||
[
|
||||
"push invalid",
|
||||
'Line 1, col 6: expected "temp", "pointer", "that", "this", "constant", "static", "local", or "argument"',
|
||||
],
|
||||
])("VM Parser Error: '%s'", (bad, message) => {
|
||||
const match = grammar.match(bad);
|
||||
expect(match).toHaveFailed(message);
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
/** Reads tst files to apply and perform test runs. */
|
||||
|
||||
import { grammar as ohmGrammar } from "ohm-js";
|
||||
import { baseSemantics, grammars, makeParser, Span, span } from "./base.js";
|
||||
|
||||
import vmGrammar from "./grammars/vm.ohm.js";
|
||||
export const grammar = ohmGrammar(vmGrammar, grammars);
|
||||
export const vmSemantics = grammar.extendSemantics(baseSemantics);
|
||||
|
||||
export interface Vm {
|
||||
instructions: VmInstruction[];
|
||||
}
|
||||
|
||||
export type Segment =
|
||||
| "argument"
|
||||
| "local"
|
||||
| "static"
|
||||
| "constant"
|
||||
| "this"
|
||||
| "that"
|
||||
| "pointer"
|
||||
| "temp";
|
||||
|
||||
export type VmInstruction =
|
||||
| StackInstruction
|
||||
| OpInstruction
|
||||
| FunctionInstruction
|
||||
| CallInstruction
|
||||
| ReturnInstruction
|
||||
| GotoInstruction
|
||||
| LabelInstruction;
|
||||
|
||||
export interface StackInstruction {
|
||||
op: "push" | "pop";
|
||||
segment: Segment;
|
||||
offset: number;
|
||||
span?: Span;
|
||||
}
|
||||
export interface OpInstruction {
|
||||
op: "add" | "sub" | "neg" | "lt" | "gt" | "eq" | "and" | "or" | "not";
|
||||
span?: Span;
|
||||
}
|
||||
export interface FunctionInstruction {
|
||||
op: "function";
|
||||
name: string;
|
||||
nVars: number;
|
||||
span?: Span;
|
||||
}
|
||||
export interface CallInstruction {
|
||||
op: "call";
|
||||
name: string;
|
||||
nArgs: number;
|
||||
span?: Span;
|
||||
}
|
||||
export interface ReturnInstruction {
|
||||
op: "return";
|
||||
span?: Span;
|
||||
}
|
||||
export interface LabelInstruction {
|
||||
op: "label";
|
||||
label: string;
|
||||
span?: Span;
|
||||
}
|
||||
export interface GotoInstruction {
|
||||
op: "goto" | "if-goto";
|
||||
label: string;
|
||||
span?: Span;
|
||||
}
|
||||
|
||||
vmSemantics.addAttribute<
|
||||
| "push"
|
||||
| "pop"
|
||||
| "function"
|
||||
| "call"
|
||||
| "return"
|
||||
| "goto"
|
||||
| "if-goto"
|
||||
| "label"
|
||||
| "add"
|
||||
| "sub"
|
||||
| "neg"
|
||||
| "lt"
|
||||
| "gt"
|
||||
| "eq"
|
||||
| "and"
|
||||
| "or"
|
||||
| "not"
|
||||
>("op", {
|
||||
push(_a, _b) {
|
||||
return "push";
|
||||
},
|
||||
pop(_a, _b) {
|
||||
return "pop";
|
||||
},
|
||||
function(_a, _b) {
|
||||
return "function";
|
||||
},
|
||||
call(_a, _b) {
|
||||
return "call";
|
||||
},
|
||||
return(_a) {
|
||||
return "return";
|
||||
},
|
||||
goto(_a, _b) {
|
||||
return "goto";
|
||||
},
|
||||
ifGoto(_a, _b) {
|
||||
return "if-goto";
|
||||
},
|
||||
label(_a, _b) {
|
||||
return "label";
|
||||
},
|
||||
Add(_) {
|
||||
return "add";
|
||||
},
|
||||
Sub(_) {
|
||||
return "sub";
|
||||
},
|
||||
Neg(_) {
|
||||
return "neg";
|
||||
},
|
||||
Eq(_) {
|
||||
return "eq";
|
||||
},
|
||||
Lt(_) {
|
||||
return "lt";
|
||||
},
|
||||
Gt(_) {
|
||||
return "gt";
|
||||
},
|
||||
And(_) {
|
||||
return "and";
|
||||
},
|
||||
Or(_) {
|
||||
return "or";
|
||||
},
|
||||
Not(_) {
|
||||
return "not";
|
||||
},
|
||||
});
|
||||
|
||||
vmSemantics.addAttribute<
|
||||
| "argument"
|
||||
| "local"
|
||||
| "static"
|
||||
| "constant"
|
||||
| "this"
|
||||
| "that"
|
||||
| "pointer"
|
||||
| "temp"
|
||||
>("segment", {
|
||||
argument(_a, _b) {
|
||||
return "argument";
|
||||
},
|
||||
local(_a, _b) {
|
||||
return "local";
|
||||
},
|
||||
static(_a, _b) {
|
||||
return "static";
|
||||
},
|
||||
constant(_a, _b) {
|
||||
return "constant";
|
||||
},
|
||||
this(_a, _b) {
|
||||
return "this";
|
||||
},
|
||||
that(_a, _b) {
|
||||
return "that";
|
||||
},
|
||||
pointer(_a, _b) {
|
||||
return "pointer";
|
||||
},
|
||||
temp(_a, _b) {
|
||||
return "temp";
|
||||
},
|
||||
});
|
||||
|
||||
vmSemantics.addAttribute<VmInstruction>("instruction", {
|
||||
StackInstruction({ op }, { segment }, value) {
|
||||
return {
|
||||
op: op as "push" | "pop",
|
||||
segment,
|
||||
offset: Number(value.sourceString),
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
OpInstruction({ op }) {
|
||||
return {
|
||||
op: op as
|
||||
| "add"
|
||||
| "sub"
|
||||
| "neg"
|
||||
| "lt"
|
||||
| "gt"
|
||||
| "eq"
|
||||
| "and"
|
||||
| "or"
|
||||
| "not",
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
FunctionInstruction(_, { name }, nVars) {
|
||||
return {
|
||||
op: "function",
|
||||
name,
|
||||
nVars: Number(nVars.sourceString),
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
CallInstruction(_, { name }, nArgs) {
|
||||
return {
|
||||
op: "call",
|
||||
name,
|
||||
nArgs: Number(nArgs.sourceString),
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
ReturnInstruction(_) {
|
||||
return { op: "return", span: span(this.source) };
|
||||
},
|
||||
// LabelInstruction = Label Name
|
||||
LabelInstruction(_, { name: label }) {
|
||||
return { op: "label", label, span: span(this.source) };
|
||||
},
|
||||
// GotoInstruction = (Goto | IfGoto) Name
|
||||
GotoInstruction({ op }, { name: label }) {
|
||||
return {
|
||||
op: op as "goto" | "if-goto",
|
||||
label,
|
||||
span: span(this.source),
|
||||
};
|
||||
},
|
||||
VmInstructionLine(inst, _) {
|
||||
return inst.instruction;
|
||||
},
|
||||
});
|
||||
|
||||
vmSemantics.addAttribute<Vm>("vm", {
|
||||
Vm(_, lines, last) {
|
||||
const instructions = lines.children.map((node) => node.instruction) ?? [];
|
||||
return {
|
||||
instructions: last.child(0)
|
||||
? [...instructions, last.child(0).instruction]
|
||||
: instructions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
vmSemantics.addAttribute<Vm>("root", {
|
||||
Root({ vm }) {
|
||||
return vm;
|
||||
},
|
||||
});
|
||||
|
||||
export const VM = {
|
||||
grammar: vmGrammar,
|
||||
semantics: vmSemantics,
|
||||
parser: grammar,
|
||||
parse: makeParser<Vm>(grammar, vmSemantics),
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { ASM } from "./languages/asm.js";
|
||||
import { int2, parseTwosInt } from "./util/twos.js";
|
||||
|
||||
export async function loadAsm(source: string): Promise<number[]> {
|
||||
const asm = unwrap(ASM.parse(source));
|
||||
ASM.passes.fillLabel(asm);
|
||||
return ASM.passes.emit(asm);
|
||||
}
|
||||
|
||||
export async function loadHack(source: string): Promise<number[]> {
|
||||
return source
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map(int2);
|
||||
}
|
||||
|
||||
export function loadHackSync(source: string): number[] {
|
||||
return source
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map(int2);
|
||||
}
|
||||
|
||||
export async function loadBlob(bytes: string): Promise<number[]> {
|
||||
return bytes
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map(parseTwosInt);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { cleanState } from "@davidsouther/jiffies/lib/esm/scope/state.js";
|
||||
import { Output } from "./output.js";
|
||||
import { TestOutputInstruction } from "./test/instruction.js";
|
||||
import { Test } from "./test/tst.js";
|
||||
|
||||
class OutputTest extends Test {
|
||||
private readonly vars: Map<string, number | string>;
|
||||
|
||||
constructor(init: [string, number | string][]) {
|
||||
super();
|
||||
this.vars = new Map(init);
|
||||
}
|
||||
|
||||
hasVar(variable: string | number): boolean {
|
||||
return this.vars.has(`${variable}`);
|
||||
}
|
||||
getVar(variable: string | number): number | string {
|
||||
return this.vars.get(`${variable}`) ?? 0;
|
||||
}
|
||||
getWidth(variable: string, offset?: number | undefined): number {
|
||||
return 1;
|
||||
}
|
||||
setVar(variable: string, value: number): void {
|
||||
this.vars.set(`${variable}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
describe("Test Output Handler", () => {
|
||||
const state = cleanState(
|
||||
() => ({
|
||||
test: new OutputTest([
|
||||
["time", "14+"],
|
||||
["a", 1],
|
||||
["b", 20],
|
||||
["in", 0],
|
||||
["out", -1],
|
||||
["address", 1234],
|
||||
]),
|
||||
}),
|
||||
beforeEach,
|
||||
);
|
||||
|
||||
it("outputs padded values", () => {
|
||||
const outA = new Output("a", "D", 1, 3, 3);
|
||||
const a = outA.print(state.test);
|
||||
|
||||
expect(a).toEqual(" 1 ");
|
||||
});
|
||||
|
||||
it("outputs 16 bit values", () => {
|
||||
const outB = new Output("b", "B", 16, 1, 1);
|
||||
const b = outB.print(state.test);
|
||||
expect(b).toEqual(" 0000000000010100 ");
|
||||
});
|
||||
|
||||
it("outputs a line", () => {
|
||||
state.test.outputList([
|
||||
{ id: "a", style: "D", len: 1, lpad: 2, rpad: 2 },
|
||||
{ id: "b", style: "X", len: 6, lpad: 1, rpad: 1 },
|
||||
{ id: "in", style: "B", len: 2, lpad: 2, rpad: 2 },
|
||||
{ id: "out", style: "B", len: 4, lpad: 2, rpad: 2 },
|
||||
]);
|
||||
state.test.addInstruction(new TestOutputInstruction());
|
||||
state.test.run();
|
||||
|
||||
expect(state.test.log()).toEqual("| 1 | 0x0014 | 00 | 1111 |\n");
|
||||
});
|
||||
|
||||
it("outputs 16 bit", () => {
|
||||
const test = new OutputTest([
|
||||
["a", 0b0001001000110100],
|
||||
["b", 0b1001100001110110],
|
||||
]);
|
||||
test.outputList([
|
||||
{ id: "a", style: "B", len: 16, lpad: 1, rpad: 1 },
|
||||
{ id: "b", style: "B", len: 16, lpad: 1, rpad: 1 },
|
||||
]);
|
||||
|
||||
test.addInstruction(new TestOutputInstruction());
|
||||
test.run();
|
||||
|
||||
expect(test.log()).toEqual("| 0001001000110100 | 1001100001110110 |\n");
|
||||
});
|
||||
|
||||
it("outputs a header for 16 bit", () => {
|
||||
const outB = new Output("b", "B", 16, 1, 1);
|
||||
const b = outB.header(state.test);
|
||||
expect(b).toEqual(" b ");
|
||||
});
|
||||
|
||||
it("truncates a narrow header", () => {
|
||||
const wideOut = new Output("addressM", "D", 5, 0, 0);
|
||||
|
||||
const wide = wideOut.header(state.test);
|
||||
expect(wide).toEqual("addre");
|
||||
});
|
||||
|
||||
it("does not center %S", () => {
|
||||
const outTime = new Output("time", "S", 6, 1, 1);
|
||||
const time = outTime.print(state.test);
|
||||
expect(`'${time}'`).toEqual("' 14+ '");
|
||||
});
|
||||
|
||||
it("outputs builtin header with no index", () => {
|
||||
const outPC = new Output("PC", "D", 4, 0, 0, true, -1);
|
||||
const header = outPC.header(state.test);
|
||||
expect(header).toEqual("PC[]");
|
||||
});
|
||||
|
||||
it("outputs builtin header with index", () => {
|
||||
const outPC = new Output("RAM16K", "D", 7, 1, 1, true, 2);
|
||||
const header = outPC.header(state.test);
|
||||
expect(header).toEqual("RAM16K[2]");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Test } from "./test/tst.js";
|
||||
import { bin, dec, hex } from "./util/twos.js";
|
||||
|
||||
export class Output {
|
||||
private readonly fmt: "B" | "X" | "D" | "S";
|
||||
private readonly lPad: number;
|
||||
private readonly rPad: number;
|
||||
private readonly len: number;
|
||||
private readonly index: number;
|
||||
private readonly builtin: boolean;
|
||||
|
||||
// new Output(inst.id, inst.style, inst.width, inst.lpad, inst.rpad)
|
||||
constructor(
|
||||
private variable: string,
|
||||
format = "%B1.1.1",
|
||||
len?: number,
|
||||
lPad?: number,
|
||||
rPad?: number,
|
||||
builtin?: boolean,
|
||||
index?: number,
|
||||
) {
|
||||
if (
|
||||
format.startsWith("%") &&
|
||||
len === undefined &&
|
||||
lPad === undefined &&
|
||||
rPad === undefined
|
||||
) {
|
||||
const { fmt, lPad, rPad, len } = format.match(
|
||||
/^%(?<fmt>[BDXS])(?<lPad>\d+)\.(?<len>\d+)\.(?<rPad>\d+)$/,
|
||||
)?.groups as {
|
||||
fmt: "B" | "X" | "D" | "S";
|
||||
lPad: string;
|
||||
rPad: string;
|
||||
len: string;
|
||||
};
|
||||
this.fmt = fmt;
|
||||
this.lPad = parseInt(lPad);
|
||||
this.rPad = parseInt(rPad);
|
||||
this.len = parseInt(len);
|
||||
this.builtin = false;
|
||||
this.index = -1;
|
||||
} else {
|
||||
assert(["B", "X", "D", "S"].includes(format[0]));
|
||||
this.fmt = format[0] as "B" | "X" | "D" | "S";
|
||||
this.len = len ?? 3;
|
||||
this.lPad = lPad ?? 1;
|
||||
this.rPad = rPad ?? 1;
|
||||
this.builtin = builtin ?? false;
|
||||
this.index = index ?? -1;
|
||||
}
|
||||
}
|
||||
|
||||
header(test: Test) {
|
||||
let variable = `${this.variable}`;
|
||||
if (this.builtin) {
|
||||
const index = this.index >= 0 ? this.index : "";
|
||||
variable = `${variable}[${index}]`;
|
||||
}
|
||||
if (variable.length > this.len + this.lPad + this.rPad) {
|
||||
return variable.substring(0, this.len + this.lPad + this.rPad);
|
||||
}
|
||||
return this.padCenter(variable);
|
||||
}
|
||||
|
||||
print(test: Test) {
|
||||
const val = test.getVar(this.variable, this.index);
|
||||
if (this.fmt === "S") {
|
||||
return this.padLeft(val as string);
|
||||
}
|
||||
|
||||
const fmt = { B: bin, D: dec, X: hex }[this.fmt];
|
||||
const value = fmt(val as number);
|
||||
if (this.fmt === "D") {
|
||||
return this.padRight(value);
|
||||
} else {
|
||||
return this.padLeft(value.slice(value.length - this.len));
|
||||
}
|
||||
}
|
||||
|
||||
private padCenter(value: string) {
|
||||
const space = this.lPad + this.len + this.rPad;
|
||||
const leftSpace = Math.floor((space - value.length) / 2);
|
||||
const rightSpace = space - leftSpace - value.length;
|
||||
const padLeft = leftSpace + value.length;
|
||||
const padRight = padLeft + rightSpace;
|
||||
value = value.padStart(padLeft);
|
||||
value = value.padEnd(padRight);
|
||||
return value;
|
||||
}
|
||||
|
||||
private padLeft(value: string) {
|
||||
value = value.substring(0, this.len);
|
||||
const padRight = this.rPad + this.len;
|
||||
const padLeft = this.lPad + padRight;
|
||||
value = value.padEnd(padRight);
|
||||
value = value.padStart(padLeft);
|
||||
return value;
|
||||
}
|
||||
|
||||
private padRight(value: string) {
|
||||
value = value.substring(0, this.len);
|
||||
const padLeft = this.lPad + this.len;
|
||||
const padRight = this.rPad + padLeft;
|
||||
value = value.padStart(padLeft);
|
||||
value = value.padEnd(padRight);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Ok, unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
ASM_PROJECTS,
|
||||
CHIP_PROJECTS,
|
||||
VM_PROJECTS,
|
||||
} from "@nand2tetris/projects/base.js";
|
||||
import { ChipProjects, VmProjects } from "@nand2tetris/projects/full.js";
|
||||
import { Max } from "@nand2tetris/projects/samples/hack.js";
|
||||
import {
|
||||
FILES as ASM_FILES,
|
||||
ASM_SOLS,
|
||||
} from "@nand2tetris/projects/samples/project_06/index.js";
|
||||
import { ChipProjects as ChipProjectsSols } from "@nand2tetris/projects/testing/index.js";
|
||||
import { build } from "../chip/builder.js";
|
||||
import { Chip } from "../chip/chip.js";
|
||||
import { compare } from "../compare.js";
|
||||
import { ASM, Asm } from "../languages/asm.js";
|
||||
import { CMP, Cmp } from "../languages/cmp.js";
|
||||
import { HDL, HdlParse } from "../languages/hdl.js";
|
||||
import { TST, Tst } from "../languages/tst.js";
|
||||
import { VM } from "../languages/vm.js";
|
||||
import { ChipTest } from "../test/chiptst.js";
|
||||
import { VMTest } from "../test/vmtst.js";
|
||||
import { Vm } from "../vm/vm.js";
|
||||
|
||||
const PROJECTS = new Set<string>(["01", "03", "07", "08"]);
|
||||
const SKIP = new Set<string>([]);
|
||||
const INCLUDE = new Set<string>(["And", "And16", "Mux8Way16", "Bit"]);
|
||||
|
||||
describe("Chip Projects", () => {
|
||||
describe.each(Object.keys(CHIP_PROJECTS).filter((k) => PROJECTS.has(k)))(
|
||||
"project %s",
|
||||
(project) => {
|
||||
it.each(
|
||||
CHIP_PROJECTS[project as keyof typeof CHIP_PROJECTS]
|
||||
.filter((k) => !SKIP.has(k))
|
||||
.filter((k) => INCLUDE.has(k)),
|
||||
)("Chip %s", async (chipName) => {
|
||||
const chipProject = {
|
||||
// @ts-ignore
|
||||
...assertExists(ChipProjects[project]),
|
||||
// @ts-ignore
|
||||
...assertExists(ChipProjectsSols[project]),
|
||||
};
|
||||
const hdlFile = chipProject.SOLS[chipName]?.[`${chipName}.hdl`];
|
||||
const tstFile = chipProject.CHIPS?.[`${chipName}.tst`];
|
||||
const cmpFile = chipProject.CHIPS?.[`${chipName}.cmp`];
|
||||
|
||||
expect(hdlFile).toBeDefined();
|
||||
expect(tstFile).toBeDefined();
|
||||
expect(cmpFile).toBeDefined();
|
||||
|
||||
const hdl = HDL.parse(hdlFile);
|
||||
expect(hdl).toBeOk();
|
||||
const tst = TST.parse(tstFile);
|
||||
expect(tst).toBeOk();
|
||||
|
||||
const chip = await build({ parts: Ok(hdl as Ok<HdlParse>) });
|
||||
expect(chip).toBeOk();
|
||||
const test = unwrap(ChipTest.from(Ok(tst as Ok<Tst>))).with(
|
||||
Ok(chip as Ok<Chip>),
|
||||
);
|
||||
|
||||
if (chipName === "Computer") {
|
||||
test.setFileSystem(
|
||||
new FileSystem(new ObjectFileSystemAdapter({ "Max.hack": Max })),
|
||||
);
|
||||
}
|
||||
|
||||
await test.run();
|
||||
|
||||
const outFile = test.log();
|
||||
|
||||
const cmp = CMP.parse(cmpFile);
|
||||
expect(cmp).toBeOk();
|
||||
const out = CMP.parse(outFile);
|
||||
expect(out).toBeOk();
|
||||
|
||||
const diffs = compare(Ok(cmp as Ok<Cmp>), Ok(out as Ok<Cmp>));
|
||||
expect(diffs).toHaveNoDiff();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("ASM Projects", () => {
|
||||
describe.each(Object.keys(ASM_PROJECTS))("project %s", (project) => {
|
||||
it.each(Object.keys(ASM_FILES))("%s", (file_name) => {
|
||||
const source = ASM_FILES[file_name as keyof typeof ASM_FILES];
|
||||
const parsed = ASM.parse(source);
|
||||
expect(parsed).toBeOk();
|
||||
const asm = Ok(parsed as Ok<Asm>);
|
||||
ASM.passes.fillLabel(asm);
|
||||
const filled = ASM.passes.emit(asm);
|
||||
expect(filled).toEqual(ASM_SOLS[file_name as keyof typeof ASM_FILES]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Vm Projects", () => {
|
||||
describe.each(Object.keys(VM_PROJECTS).filter((k) => PROJECTS.has(k)))(
|
||||
"project %s",
|
||||
(project) => {
|
||||
it.each(
|
||||
VM_PROJECTS[project as keyof typeof VM_PROJECTS].filter(
|
||||
(k) => !SKIP.has(k),
|
||||
),
|
||||
)("VM Program %s", async (vmName) => {
|
||||
const vmProject = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...assertExists(VmProjects[project]),
|
||||
};
|
||||
|
||||
const tstFile = vmProject.VMS[vmName]?.[`${vmName}VME.tst`];
|
||||
const cmpFile = vmProject.VMS[vmName]?.[`${vmName}.cmp`];
|
||||
|
||||
let vmCode = "";
|
||||
for (const filename of Object.keys(vmProject.VMS[vmName])) {
|
||||
if (filename.endsWith(".vm")) {
|
||||
const vmFile = vmProject.VMS[vmName]?.[filename];
|
||||
expect(vmFile).toBeDefined();
|
||||
vmCode += vmFile;
|
||||
}
|
||||
}
|
||||
|
||||
expect(tstFile).toBeDefined();
|
||||
expect(cmpFile).toBeDefined();
|
||||
|
||||
const parsed = VM.parse(vmCode);
|
||||
expect(parsed).toBeOk();
|
||||
const tst = TST.parse(tstFile);
|
||||
expect(tst).toBeOk();
|
||||
|
||||
const vm = await Vm.build(unwrap(parsed).instructions);
|
||||
expect(vm).toBeOk();
|
||||
const test = unwrap(VMTest.from(unwrap(tst))).with(unwrap(vm));
|
||||
|
||||
await test.run();
|
||||
|
||||
const outFile = test.log();
|
||||
|
||||
const cmp = CMP.parse(cmpFile);
|
||||
expect(cmp).toBeOk();
|
||||
const out = CMP.parse(outFile);
|
||||
expect(out).toBeOk();
|
||||
|
||||
const diffs = compare(unwrap(cmp), unwrap(out));
|
||||
expect(diffs).toHaveNoDiff();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
isOk,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
type Assignment,
|
||||
AssignmentStubs,
|
||||
} from "@nand2tetris/projects/base.js";
|
||||
import type { Runner, RunResult } from "@nand2tetris/runner/types.js";
|
||||
import { build as buildChip } from "../chip/builder.js";
|
||||
import { Chip } from "../chip/chip.js";
|
||||
import { CompilationError } from "../languages/base.js";
|
||||
import { HDL, HdlParse } from "../languages/hdl.js";
|
||||
import { TST, Tst } from "../languages/tst.js";
|
||||
import { ChipTest } from "../test/chiptst.js";
|
||||
|
||||
export interface AssignmentFiles extends Assignment {
|
||||
hdl: string;
|
||||
tst: string;
|
||||
cmp: string;
|
||||
}
|
||||
|
||||
export interface AssignmentParse extends AssignmentFiles {
|
||||
maybeParsedHDL: Result<HdlParse, CompilationError>;
|
||||
maybeParsedTST: Result<Tst, CompilationError>;
|
||||
}
|
||||
|
||||
export interface AssignmentBuild extends AssignmentParse {
|
||||
maybeChip: Result<Chip, Error>;
|
||||
maybeTest: Result<ChipTest, Error>;
|
||||
}
|
||||
|
||||
export interface AssignmentRun extends AssignmentBuild {
|
||||
pass: boolean;
|
||||
out: string;
|
||||
shadow?: RunResult;
|
||||
}
|
||||
|
||||
export const hasTest = ({
|
||||
name,
|
||||
ext,
|
||||
}: {
|
||||
name: string;
|
||||
ext: string;
|
||||
}): boolean =>
|
||||
AssignmentStubs[name as keyof typeof AssignmentStubs] !== undefined &&
|
||||
[".hdl", ".tst"].includes(ext);
|
||||
|
||||
/** Try parsing the loaded files. */
|
||||
export const maybeParse = (file: AssignmentFiles): AssignmentParse => {
|
||||
const maybeParsedHDL = HDL.parse(file.hdl);
|
||||
const maybeParsedTST = TST.parse(file.tst);
|
||||
return { ...file, maybeParsedHDL, maybeParsedTST };
|
||||
};
|
||||
|
||||
/** After parsing the assignment, compile the Chip and Tst. */
|
||||
export const maybeBuild =
|
||||
(fs: FileSystem) =>
|
||||
async (file: AssignmentParse): Promise<AssignmentBuild> => {
|
||||
let maybeChip: Result<Chip, Error>;
|
||||
if (isOk(file.maybeParsedHDL)) {
|
||||
const maybeBuilt = await buildChip({
|
||||
parts: Ok(file.maybeParsedHDL),
|
||||
fs,
|
||||
});
|
||||
if (isErr(maybeBuilt)) {
|
||||
maybeChip = Err(new Error(Err(maybeBuilt).message));
|
||||
} else {
|
||||
maybeChip = maybeBuilt;
|
||||
}
|
||||
} else {
|
||||
maybeChip = Err(new Error("HDL Was not parsed"));
|
||||
}
|
||||
const maybeTest = isOk(file.maybeParsedTST)
|
||||
? ChipTest.from(Ok(file.maybeParsedTST))
|
||||
: Err(new Error("TST Was not parsed"));
|
||||
|
||||
return { ...file, maybeChip, maybeTest };
|
||||
};
|
||||
|
||||
/** If the assignment parsed, run it! */
|
||||
export const tryRun =
|
||||
(fs: FileSystem) =>
|
||||
async (assignment: AssignmentBuild): Promise<AssignmentRun> => {
|
||||
if (isErr(assignment.maybeChip)) {
|
||||
return {
|
||||
...assignment,
|
||||
pass: false,
|
||||
out: Err(assignment.maybeChip).message,
|
||||
};
|
||||
}
|
||||
if (isErr(assignment.maybeTest)) {
|
||||
return {
|
||||
...assignment,
|
||||
pass: false,
|
||||
out: Err(assignment.maybeTest).message,
|
||||
};
|
||||
}
|
||||
const test = Ok(assignment.maybeTest)
|
||||
.with(Ok(assignment.maybeChip))
|
||||
.setFileSystem(fs);
|
||||
await test.run();
|
||||
const out = test.log();
|
||||
const pass = out.trim() === assignment.cmp.trim();
|
||||
return { ...assignment, out, pass };
|
||||
};
|
||||
|
||||
/** Parse & execute a Nand2tetris assignment, possibly also including the Java output in shadow mode. */
|
||||
export const runner = (fs: FileSystem, ideRunner?: Runner) => {
|
||||
const tryRunWithFs = tryRun(fs);
|
||||
const maybeBuildWithFs = maybeBuild(fs);
|
||||
return async (assignment: AssignmentFiles): Promise<AssignmentRun> => {
|
||||
const jsRunner = async () =>
|
||||
tryRunWithFs(await maybeBuildWithFs(await maybeParse(assignment)));
|
||||
const javaRunner = async () => ideRunner?.hdl(assignment);
|
||||
|
||||
const [jsRun, shadow] = await Promise.all([jsRunner(), javaRunner()]);
|
||||
return { ...jsRun, shadow };
|
||||
};
|
||||
};
|
||||
|
||||
/** Run all tests for a given Nand2Tetris project. */
|
||||
export async function runTests(
|
||||
files: Array<Assignment>,
|
||||
loadAssignment: (file: Assignment) => Promise<AssignmentFiles>,
|
||||
fs: FileSystem,
|
||||
ideRunner?: Runner,
|
||||
): Promise<AssignmentRun[]> {
|
||||
const run = runner(fs, ideRunner);
|
||||
return Promise.all(
|
||||
files.map(loadAssignment).map(async (assignment) => run(await assignment)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
isOk,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import type { MatchResult } from "ohm-js";
|
||||
import { Diff } from "./compare.js";
|
||||
|
||||
interface CustomMatchers<R = unknown, T = unknown> {
|
||||
toBeOk(expected?: T): R;
|
||||
toBeErr(expected?: T): R;
|
||||
}
|
||||
|
||||
interface OhmMatchers<R = unknown> {
|
||||
toHaveSucceeded(): R;
|
||||
toHaveFailed(message: string): R;
|
||||
}
|
||||
|
||||
interface CmpMatchers<R = unknown> {
|
||||
toHaveNoDiff(): R;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// biome-ignore lint/style/noNamespace: add some setup stuff
|
||||
namespace jest {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
type Expect = CustomMatchers;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
interface Matchers<R, T = unknown>
|
||||
extends CustomMatchers<R, T>,
|
||||
OhmMatchers<R>,
|
||||
CmpMatchers<R> {}
|
||||
interface InverseAsymmetricMatchers extends CustomMatchers, OhmMatchers {}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({
|
||||
toBeErr<R>(result: Result<R>, expected?: Err<R>) {
|
||||
if (isOk(result)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () =>
|
||||
`Expected Err(${display(expected)}), got Ok(${display(Err(result))})`,
|
||||
};
|
||||
} else {
|
||||
if (expected) {
|
||||
expect(Err(result)).toMatchObject(Err(expected) as Error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Err(${display(Err(result))}) is expected`,
|
||||
};
|
||||
},
|
||||
toBeOk<R>(result: Result<R>, expected?: Ok<R>) {
|
||||
if (isErr(result)) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () =>
|
||||
`Expected Ok(${display(expected)}), got Err(${display(Err(result))})`,
|
||||
};
|
||||
} else {
|
||||
if (expected) {
|
||||
expect<R>(Ok(result)).toMatchObject(Ok(expected) as object);
|
||||
}
|
||||
}
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `Ok(${display(Ok(result))}) is expected`,
|
||||
};
|
||||
},
|
||||
toHaveSucceeded(match: MatchResult) {
|
||||
if (match.succeeded()) {
|
||||
return { pass: true, message: () => "Match succeeded" };
|
||||
} else {
|
||||
return { pass: false, message: () => match.message ?? "Match failed" };
|
||||
}
|
||||
},
|
||||
toHaveFailed(match: MatchResult, message: string) {
|
||||
expect(match.failed()).toBe(true);
|
||||
expect(match.shortMessage).toBe(message);
|
||||
return {
|
||||
pass: true,
|
||||
message: () => "Failed to parse with correct message",
|
||||
};
|
||||
},
|
||||
toHaveNoDiff(diffs: Diff[]) {
|
||||
expect(
|
||||
diffs.map(({ a, b, col, row }) => `${a} <> ${b} (${row}:${col})`),
|
||||
).toEqual([]);
|
||||
return {
|
||||
pass: true,
|
||||
message: () => "There were no diffs",
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import { checkExhaustive } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { Span } from "../languages/base.js";
|
||||
import {
|
||||
Tst,
|
||||
TstCommand,
|
||||
TstOperation,
|
||||
TstStatement,
|
||||
TstWhileStatement,
|
||||
} from "../languages/tst.js";
|
||||
import {
|
||||
TestEvalInstruction,
|
||||
TestTickInstruction,
|
||||
TestTockInstruction,
|
||||
} from "./chiptst.js";
|
||||
import { TestResetRamInstruction, TestTickTockInstruction } from "./cputst.js";
|
||||
import {
|
||||
Condition,
|
||||
TestBreakInstruction,
|
||||
TestClearEchoInstruction,
|
||||
TestCompareToInstruction,
|
||||
TestEchoInstruction,
|
||||
TestInstruction,
|
||||
TestLoadInstruction,
|
||||
TestLoadROMInstruction,
|
||||
TestOutputFileInstruction,
|
||||
TestOutputInstruction,
|
||||
TestOutputListInstruction,
|
||||
TestRepeatInstruction,
|
||||
TestSetInstruction,
|
||||
TestStopInstruction,
|
||||
TestWhileInstruction,
|
||||
} from "./instruction.js";
|
||||
import { Test } from "./tst.js";
|
||||
import { TestVMStepInstruction } from "./vmtst.js";
|
||||
|
||||
export function isTstCommand(line: TstStatement): line is TstCommand {
|
||||
return (line as TstCommand).op !== undefined;
|
||||
}
|
||||
|
||||
function isTstWhileStatement(line: TstStatement): line is TstWhileStatement {
|
||||
return (line as TstWhileStatement).condition !== undefined;
|
||||
}
|
||||
|
||||
function makeInstruction(inst: TstOperation) {
|
||||
const { op } = inst;
|
||||
switch (op) {
|
||||
case "tick":
|
||||
return new TestTickInstruction();
|
||||
case "tock":
|
||||
return new TestTockInstruction();
|
||||
case "ticktock":
|
||||
return new TestTickTockInstruction();
|
||||
case "eval":
|
||||
return new TestEvalInstruction();
|
||||
case "vmstep":
|
||||
return new TestVMStepInstruction();
|
||||
case "output":
|
||||
return new TestOutputInstruction();
|
||||
case "set":
|
||||
return new TestSetInstruction(inst.id, inst.value, inst.index);
|
||||
case "output-list":
|
||||
return new TestOutputListInstruction(inst.spec);
|
||||
case "echo":
|
||||
return new TestEchoInstruction(inst.message);
|
||||
case "clear-echo":
|
||||
return new TestClearEchoInstruction();
|
||||
case "loadRom":
|
||||
return new TestLoadROMInstruction(inst.file);
|
||||
case "load":
|
||||
return new TestLoadInstruction(inst.file);
|
||||
case "output-file":
|
||||
return new TestOutputFileInstruction(inst.file);
|
||||
case "compare-to":
|
||||
return new TestCompareToInstruction(inst.file);
|
||||
case "resetRam":
|
||||
return new TestResetRamInstruction();
|
||||
default:
|
||||
checkExhaustive(op, `Unknown tst operation ${op}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function fill<T extends Test>(
|
||||
test: T,
|
||||
tst: Tst,
|
||||
requireLoad = true,
|
||||
): Result<T, Error> {
|
||||
let span: Span | undefined;
|
||||
let stepInstructions: TestInstruction[] = [];
|
||||
|
||||
let base: T | TestWhileInstruction | TestRepeatInstruction = test;
|
||||
let commands: TstCommand[] = [];
|
||||
|
||||
let hasLoad = false;
|
||||
|
||||
for (const line of tst.lines) {
|
||||
if (isTstCommand(line)) {
|
||||
base = test;
|
||||
commands = [line];
|
||||
} else {
|
||||
const repeat = isTstWhileStatement(line)
|
||||
? new TestWhileInstruction(
|
||||
new Condition(
|
||||
line.condition.left,
|
||||
line.condition.right,
|
||||
line.condition.op,
|
||||
),
|
||||
)
|
||||
: new TestRepeatInstruction(line.count);
|
||||
repeat.span = line.span;
|
||||
test.addInstruction(repeat);
|
||||
|
||||
base = repeat;
|
||||
commands = line.statements;
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
if (command.op.op == "load") {
|
||||
hasLoad = true;
|
||||
}
|
||||
const inst = makeInstruction(command.op);
|
||||
if (inst !== undefined) {
|
||||
if (span === undefined) {
|
||||
span = line.span;
|
||||
} else {
|
||||
span.end = line.span.end;
|
||||
}
|
||||
|
||||
base.addInstruction(inst);
|
||||
stepInstructions.push(inst);
|
||||
}
|
||||
if (command.separator != ",") {
|
||||
if (command.separator == ";") {
|
||||
base.addInstruction(new TestStopInstruction(span ?? command.span));
|
||||
} else if (command.separator == "!") {
|
||||
base.addInstruction(new TestBreakInstruction(span ?? command.span));
|
||||
}
|
||||
for (const inst of stepInstructions) {
|
||||
inst.span = span ?? command.span;
|
||||
}
|
||||
span = undefined;
|
||||
stepInstructions = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requireLoad && !hasLoad) {
|
||||
return Err(new Error("A test script must have a load command"));
|
||||
}
|
||||
|
||||
test.reset();
|
||||
|
||||
return Ok(test);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { Computer } from "../chip/builtins/computer/computer.js";
|
||||
import { Nand } from "../chip/builtins/logic/nand.js";
|
||||
import { TstRepeat } from "../languages/tst.js";
|
||||
import {
|
||||
ChipTest,
|
||||
TestEvalInstruction,
|
||||
TestTickInstruction,
|
||||
TestTockInstruction,
|
||||
} from "./chiptst.js";
|
||||
import {
|
||||
TestCompoundInstruction,
|
||||
TestOutputInstruction,
|
||||
TestSetInstruction,
|
||||
} from "./instruction.js";
|
||||
|
||||
describe("Chip Test", () => {
|
||||
describe("Builtins", () => {
|
||||
it("can set Memory", async () => {
|
||||
const computer = new Computer();
|
||||
const test = new ChipTest().with(computer);
|
||||
|
||||
test.addInstruction(new TestSetInstruction("RAM16K", 0x1234, 2));
|
||||
await test.run();
|
||||
|
||||
expect(computer.get("RAM16K", 2)?.busVoltage).toBe(0x1234);
|
||||
});
|
||||
|
||||
it("can read memory", async () => {
|
||||
const computer = new Computer();
|
||||
const test = new ChipTest().with(computer);
|
||||
test.outputList([
|
||||
{
|
||||
id: "RAM16K",
|
||||
style: "D",
|
||||
len: 4,
|
||||
lpad: 0,
|
||||
rpad: 0,
|
||||
builtin: true,
|
||||
address: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
test.addInstruction(new TestSetInstruction("RAM16K", 1234, 2));
|
||||
test.addInstruction(new TestOutputInstruction());
|
||||
await test.run();
|
||||
|
||||
expect(test.log()).toEqual(`|1234|\n`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Full tests", () => {
|
||||
it("creates a simulator test", async () => {
|
||||
const test = new ChipTest().with(new Nand());
|
||||
test.outputList(
|
||||
["a", "b", "out"].map((v) => {
|
||||
return { id: v };
|
||||
}),
|
||||
);
|
||||
|
||||
let statement: TestCompoundInstruction;
|
||||
statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
[
|
||||
new TestSetInstruction("a", 0),
|
||||
new TestSetInstruction("b", 0),
|
||||
new TestEvalInstruction(),
|
||||
new TestOutputInstruction(),
|
||||
].forEach((i) => statement.addInstruction(i));
|
||||
|
||||
statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
[
|
||||
new TestSetInstruction("a", 1),
|
||||
new TestSetInstruction("b", 1),
|
||||
new TestEvalInstruction(),
|
||||
new TestOutputInstruction(),
|
||||
].forEach((i) => statement.addInstruction(i));
|
||||
|
||||
statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
[
|
||||
new TestSetInstruction("a", 1),
|
||||
new TestSetInstruction("b", 0),
|
||||
new TestEvalInstruction(),
|
||||
new TestOutputInstruction(),
|
||||
].forEach((i) => statement.addInstruction(i));
|
||||
|
||||
statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
[
|
||||
new TestSetInstruction("a", 0),
|
||||
new TestSetInstruction("b", 1),
|
||||
new TestEvalInstruction(),
|
||||
new TestOutputInstruction(),
|
||||
].forEach((i) => statement.addInstruction(i));
|
||||
|
||||
await test.run();
|
||||
expect(test.log()).toEqual(
|
||||
`| 0 | 0 | 1 |\n| 1 | 1 | 0 |\n| 1 | 0 | 1 |\n| 0 | 1 | 1 |\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("tick tocks a clock", async () => {
|
||||
const test = new ChipTest(); //.with(new DFF());
|
||||
test.outputList([{ id: "time", style: "S", len: 4, lpad: 0, rpad: 0 }]);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
statement.addInstruction(new TestTickInstruction());
|
||||
statement.addInstruction(new TestOutputInstruction());
|
||||
statement.addInstruction(new TestTockInstruction());
|
||||
statement.addInstruction(new TestOutputInstruction());
|
||||
}
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
statement.addInstruction(new TestEvalInstruction());
|
||||
statement.addInstruction(new TestOutputInstruction());
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const statement = new TestCompoundInstruction();
|
||||
test.addInstruction(statement);
|
||||
statement.addInstruction(new TestTickInstruction());
|
||||
statement.addInstruction(new TestTockInstruction());
|
||||
statement.addInstruction(new TestOutputInstruction());
|
||||
}
|
||||
|
||||
await test.run();
|
||||
|
||||
expect(test.log().trim().split("\n")).toEqual(
|
||||
[
|
||||
"0+",
|
||||
"1",
|
||||
"1+",
|
||||
"2",
|
||||
"2+",
|
||||
"3",
|
||||
"3+",
|
||||
"4",
|
||||
"4+",
|
||||
"5",
|
||||
"5",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
].map((i) => `|${i.padEnd(4, " ")}|`),
|
||||
);
|
||||
});
|
||||
|
||||
it("tick tocks a clock with a repeat", async () => {
|
||||
const repeat: TstRepeat = {
|
||||
count: 5,
|
||||
statements: [
|
||||
{
|
||||
op: { op: "tick" },
|
||||
separator: ",",
|
||||
span: { start: 0, end: 27, line: 1 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ",",
|
||||
span: { start: 0, end: 27, line: 1 },
|
||||
},
|
||||
{
|
||||
op: { op: "tock" },
|
||||
separator: ",",
|
||||
span: { start: 0, end: 27, line: 1 },
|
||||
},
|
||||
{
|
||||
op: { op: "output" },
|
||||
separator: ";",
|
||||
span: { start: 0, end: 27, line: 1 },
|
||||
},
|
||||
],
|
||||
span: {
|
||||
line: 1,
|
||||
start: 0,
|
||||
end: 27,
|
||||
},
|
||||
};
|
||||
|
||||
const maybeTest = ChipTest.from(
|
||||
{
|
||||
lines: [repeat],
|
||||
},
|
||||
{ requireLoad: false },
|
||||
);
|
||||
expect(maybeTest).toBeOk();
|
||||
const test = unwrap(maybeTest);
|
||||
test.outputList([{ id: "time", style: "S", len: 4, lpad: 0, rpad: 0 }]);
|
||||
|
||||
await test.run();
|
||||
|
||||
expect(test.log().trim().split("\n")).toEqual(
|
||||
["0+", "1", "1+", "2", "2+", "3", "3+", "4", "4+", "5"].map(
|
||||
(i) => `|${i.padEnd(4, " ")}|`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("has a first step", () => {
|
||||
const test = new ChipTest(); //.with(new DFF());
|
||||
const statement = new TestSetInstruction("a", 1);
|
||||
test.addInstruction(statement);
|
||||
|
||||
test.reset();
|
||||
|
||||
expect(test.currentStep).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { Bus, Chip, HIGH, LOW, Low } from "../chip/chip.js";
|
||||
import { Clock } from "../chip/clock.js";
|
||||
import { Tst } from "../languages/tst.js";
|
||||
import { Action } from "../types.js";
|
||||
import { fill } from "./builder.js";
|
||||
import { TestInstruction } from "./instruction.js";
|
||||
import { Test } from "./tst.js";
|
||||
|
||||
export class ChipTest extends Test<ChipTestInstruction> {
|
||||
private chip: Chip = new Low();
|
||||
private doLoad?: (path: string) => Promise<Chip>;
|
||||
|
||||
get chipId(): number {
|
||||
return this.chip.id;
|
||||
}
|
||||
|
||||
private clock = Clock.get();
|
||||
|
||||
static from(
|
||||
tst: Tst,
|
||||
options: {
|
||||
dir?: string;
|
||||
setStatus?: Action<string>;
|
||||
loadAction?: (path: string) => Promise<Chip>;
|
||||
compareTo?: Action<string>;
|
||||
requireLoad?: boolean;
|
||||
} = {},
|
||||
): Result<ChipTest, Error> {
|
||||
const test = new ChipTest(options);
|
||||
|
||||
return fill(test, tst, options.requireLoad);
|
||||
}
|
||||
|
||||
constructor({
|
||||
dir,
|
||||
setStatus,
|
||||
loadAction,
|
||||
compareTo,
|
||||
}: {
|
||||
dir?: string;
|
||||
setStatus?: Action<string>;
|
||||
loadAction?: (path: string) => Promise<Chip>;
|
||||
compareTo?: Action<string>;
|
||||
} = {}) {
|
||||
super(dir, setStatus, compareTo);
|
||||
this.doLoad = loadAction;
|
||||
}
|
||||
|
||||
with(chip: Chip): this {
|
||||
this.chip = chip;
|
||||
return this;
|
||||
}
|
||||
|
||||
override async load(filename?: string): Promise<void> {
|
||||
if (!this.dir) return;
|
||||
const chip = await this.doLoad?.(
|
||||
filename ? `${this.dir}/${filename}` : this.dir,
|
||||
);
|
||||
if (chip) {
|
||||
this.chip = chip;
|
||||
}
|
||||
}
|
||||
|
||||
hasVar(variable: string | number): boolean {
|
||||
if (variable === "time") {
|
||||
return true;
|
||||
}
|
||||
variable = `${variable}`;
|
||||
// Look up built-in chip state variables
|
||||
return this.chip.hasIn(variable) || this.chip.hasOut(variable);
|
||||
}
|
||||
|
||||
getVar(variable: string | number, offset?: number): number | string {
|
||||
variable = `${variable}`;
|
||||
if (variable === "time") {
|
||||
return this.clock.toString();
|
||||
}
|
||||
const pin = this.chip.get(variable, offset);
|
||||
if (!pin) return 0;
|
||||
return pin instanceof Bus ? pin.busVoltage : pin.voltage();
|
||||
}
|
||||
|
||||
getWidth(variable: string, offset?: number): number {
|
||||
const pin = this.chip.get(variable, offset);
|
||||
if (!pin) return 0;
|
||||
return pin.width;
|
||||
}
|
||||
|
||||
setVar(variable: string, value: number, offset?: number): void {
|
||||
// Look up built-in chip state variables
|
||||
const pinOrBus = this.chip.get(variable, offset);
|
||||
if (pinOrBus instanceof Bus) {
|
||||
pinOrBus.busVoltage = value;
|
||||
} else {
|
||||
pinOrBus?.pull(value === 0 ? LOW : HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
eval(): void {
|
||||
this.chip.eval();
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
this.chip.eval();
|
||||
this.clock.tick();
|
||||
}
|
||||
|
||||
tock(): void {
|
||||
this.chip.eval();
|
||||
this.clock.tock();
|
||||
}
|
||||
|
||||
override async loadROM(filename: string) {
|
||||
await this.chip.load(this.fs, [this.dir ?? "", filename].join("/"));
|
||||
}
|
||||
|
||||
override async run() {
|
||||
this.clock.reset();
|
||||
await super.run();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChipTestInstruction extends TestInstruction {
|
||||
_chipTestInstruction_: true;
|
||||
do(test: ChipTest): Promise<void>;
|
||||
}
|
||||
|
||||
export class TestEvalInstruction implements ChipTestInstruction {
|
||||
readonly _chipTestInstruction_ = true;
|
||||
async do(test: ChipTest) {
|
||||
test.eval();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestTickInstruction implements ChipTestInstruction {
|
||||
readonly _chipTestInstruction_ = true;
|
||||
async do(test: ChipTest) {
|
||||
test.tick();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestTockInstruction implements ChipTestInstruction {
|
||||
readonly _chipTestInstruction_ = true;
|
||||
async do(test: ChipTest) {
|
||||
test.tock();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CPU } from "../cpu/cpu.js";
|
||||
import { ROM } from "../cpu/memory.js";
|
||||
import { Tst } from "../languages/tst.js";
|
||||
import { Action, AsyncAction } from "../types.js";
|
||||
import { fill, isTstCommand } from "./builder.js";
|
||||
import { TestInstruction } from "./instruction.js";
|
||||
import { Test } from "./tst.js";
|
||||
|
||||
export class CPUTest extends Test<CPUTestInstruction> {
|
||||
cpu: CPU;
|
||||
private ticks = 0;
|
||||
private doLoad?: AsyncAction<string>;
|
||||
fileLoaded = false;
|
||||
hasLoad = false;
|
||||
|
||||
static from(
|
||||
tst: Tst,
|
||||
options: {
|
||||
dir?: string;
|
||||
rom?: ROM;
|
||||
doEcho?: Action<string>;
|
||||
doLoad?: AsyncAction<string>;
|
||||
compareTo?: Action<string>;
|
||||
requireLoad?: boolean;
|
||||
} = {},
|
||||
): Result<CPUTest, Error> {
|
||||
const test = new CPUTest(options);
|
||||
|
||||
test.hasLoad = tst.lines.some(
|
||||
(line) => isTstCommand(line) && line.op.op == "load",
|
||||
);
|
||||
|
||||
return fill(test, tst, options.requireLoad);
|
||||
}
|
||||
|
||||
constructor({
|
||||
dir,
|
||||
rom = new ROM(),
|
||||
doEcho,
|
||||
doLoad,
|
||||
compareTo,
|
||||
}: {
|
||||
dir?: string;
|
||||
rom?: ROM;
|
||||
doEcho?: Action<string>;
|
||||
doLoad?: AsyncAction<string>;
|
||||
compareTo?: Action<string>;
|
||||
} = {}) {
|
||||
super(dir, doEcho, compareTo);
|
||||
this.doLoad = doLoad;
|
||||
this.cpu = new CPU({ ROM: rom });
|
||||
this.reset();
|
||||
}
|
||||
|
||||
override async step() {
|
||||
if (!this.hasLoad && this.cpu.ROM.isEmpty()) {
|
||||
throw new Error(
|
||||
"Cannot execute the test without first loading an .asm or .hack file",
|
||||
);
|
||||
}
|
||||
return super.step();
|
||||
}
|
||||
|
||||
override async load(filename?: string): Promise<void> {
|
||||
if (!filename && !this.dir) return;
|
||||
const dir = assertExists(this.dir?.split("/").slice(0, -1).join("/"));
|
||||
const rom = await this.doLoad?.(filename ? `${dir}/${filename}` : dir);
|
||||
if (rom) {
|
||||
this.cpu = new CPU({ ROM: rom });
|
||||
}
|
||||
}
|
||||
|
||||
override reset(): this {
|
||||
super.reset();
|
||||
this.cpu.reset();
|
||||
this.ticks = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
hasVar(variable: string | number): boolean {
|
||||
if (typeof variable === "number") {
|
||||
return false;
|
||||
}
|
||||
// A: Current value of the address register (unsigned 15-bit);
|
||||
// D: Current value of the data register (16-bit);
|
||||
// PC: Current value of the Program Counter (unsigned 15-bit);
|
||||
// RAM[i]: Current value of RAM location i (16-bit);
|
||||
// time: Number of time units (also called clock cycles, or ticktocks) that elapsed since the simulation started (a read-only system variable).
|
||||
if (
|
||||
variable === "A" ||
|
||||
variable === "D" ||
|
||||
variable === "PC" ||
|
||||
variable === "time" ||
|
||||
variable.startsWith("RAM")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getVar(variable: string | number, offset?: number): number {
|
||||
switch (variable) {
|
||||
case "A":
|
||||
return this.cpu.A;
|
||||
case "D":
|
||||
return this.cpu.D;
|
||||
case "PC":
|
||||
return this.cpu.PC;
|
||||
case "time":
|
||||
return this.ticks;
|
||||
case "RAM":
|
||||
// Exact RAM with offset
|
||||
return offset === undefined ? 0 : this.cpu.RAM.get(offset);
|
||||
}
|
||||
if (typeof variable === "number") return 0;
|
||||
if (variable.startsWith("RAM")) {
|
||||
// RAM with implicit offset, EG: RAM[123]
|
||||
const num = Number(variable.substring(4, variable.length - 1));
|
||||
return this.cpu.RAM.get(num);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getWidth(variable: string, offset?: number): number {
|
||||
return 16;
|
||||
}
|
||||
|
||||
setVar(variable: string, value: number, index?: number): void {
|
||||
// A: Current value of the address register (unsigned 15-bit);
|
||||
// D: Current value of the data register (16-bit);
|
||||
// PC: Current value of the Program Counter (unsigned 15-bit);
|
||||
// RAM[i]: Current value of RAM location i (16-bit);
|
||||
switch (variable) {
|
||||
case "A":
|
||||
this.cpu.setA(value);
|
||||
break;
|
||||
case "D":
|
||||
this.cpu.setD(value);
|
||||
break;
|
||||
case "PC":
|
||||
this.cpu.setPC(value);
|
||||
break;
|
||||
case "RAM":
|
||||
this.cpu.RAM.set(index ?? 0, value);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ticktock(): void {
|
||||
this.ticks += 1;
|
||||
this.cpu.tick();
|
||||
}
|
||||
|
||||
override async loadROM(filename: string): Promise<void> {
|
||||
await this.cpu.ROM.load(this.fs, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CPUTestInstruction extends TestInstruction {
|
||||
_cpuTestInstruction_: true;
|
||||
do(test: CPUTest): Promise<void>;
|
||||
}
|
||||
|
||||
export class TestTickTockInstruction implements CPUTestInstruction {
|
||||
readonly _cpuTestInstruction_ = true;
|
||||
async do(test: CPUTest) {
|
||||
test.ticktock();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestResetRamInstruction implements CPUTestInstruction {
|
||||
readonly _cpuTestInstruction_ = true;
|
||||
async do(test: CPUTest) {
|
||||
test.cpu.RAM.reset();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { Span } from "../languages/base.js";
|
||||
import { TstOutputSpec } from "../languages/tst.js";
|
||||
import { Test } from "./tst.js";
|
||||
|
||||
export interface TestInstruction {
|
||||
span?: Span;
|
||||
do(test: Test): Promise<void>;
|
||||
steps(test: Test): IterableIterator<TestInstruction>;
|
||||
}
|
||||
|
||||
export class TestControlInstruction implements TestInstruction {
|
||||
span: Span;
|
||||
|
||||
constructor(span: Span) {
|
||||
this.span = span;
|
||||
}
|
||||
|
||||
async do() {
|
||||
return;
|
||||
}
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestStopInstruction extends TestControlInstruction {}
|
||||
export class TestBreakInstruction extends TestControlInstruction {}
|
||||
|
||||
export class TestSetInstruction implements TestInstruction {
|
||||
constructor(
|
||||
private variable: string,
|
||||
private value: number,
|
||||
private index?: number | undefined,
|
||||
) {}
|
||||
|
||||
async do(test: Test) {
|
||||
test.setVar(this.variable, this.value, this.index);
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestOutputInstruction implements TestInstruction {
|
||||
async do(test: Test) {
|
||||
test.output();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OutputParams {
|
||||
id: string;
|
||||
style?: "B" | "D" | "S" | "X";
|
||||
len?: number;
|
||||
lpad?: number;
|
||||
rpad?: number;
|
||||
builtin?: boolean;
|
||||
address?: number;
|
||||
}
|
||||
|
||||
export class TestOutputListInstruction implements TestInstruction {
|
||||
private outputs: OutputParams[] = [];
|
||||
|
||||
constructor(specs: TstOutputSpec[] = []) {
|
||||
for (const spec of specs) {
|
||||
this.addOutput(spec);
|
||||
}
|
||||
}
|
||||
|
||||
addOutput(inst: TstOutputSpec) {
|
||||
this.outputs.push({
|
||||
id: inst.id,
|
||||
style: inst.format?.style ?? "B",
|
||||
len: inst.format?.width ?? -1,
|
||||
lpad: inst.format?.lpad ?? 1,
|
||||
rpad: inst.format?.rpad ?? 1,
|
||||
builtin: inst.builtin,
|
||||
address: inst.address,
|
||||
});
|
||||
}
|
||||
|
||||
async do(test: Test) {
|
||||
test.outputList(this.outputs);
|
||||
test.header();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestCompoundInstruction implements TestInstruction {
|
||||
protected readonly instructions: TestInstruction[] = [];
|
||||
span?: Span;
|
||||
|
||||
addInstruction(instruction: TestInstruction) {
|
||||
this.instructions.push(instruction);
|
||||
}
|
||||
|
||||
async do(test: Test<TestInstruction>) {
|
||||
for (const instruction of this.instructions) {
|
||||
instruction.do(test);
|
||||
}
|
||||
}
|
||||
|
||||
*steps(_test: Test): Generator<TestInstruction> {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestRepeatInstruction extends TestCompoundInstruction {
|
||||
constructor(public readonly repeat: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async do() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private *innerSteps(test: Test): Generator<TestInstruction> {
|
||||
for (const instruction of this.instructions) {
|
||||
yield* instruction.steps(test) as Generator<TestInstruction>;
|
||||
}
|
||||
}
|
||||
|
||||
override *steps(test: Test): Generator<TestInstruction> {
|
||||
if (this.repeat === -1) {
|
||||
yield this;
|
||||
while (true) {
|
||||
yield* this.innerSteps(test);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.repeat; i++) {
|
||||
yield this;
|
||||
yield* this.innerSteps(test);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Condition {
|
||||
constructor(
|
||||
public readonly x: string | number,
|
||||
public readonly y: string | number,
|
||||
public readonly op: "<" | "<=" | "=" | ">=" | ">" | "<>",
|
||||
) {}
|
||||
|
||||
check(test: Test): boolean {
|
||||
const x = test.hasVar(this.x) ? test.getVar(this.x) : this.x;
|
||||
const y = test.hasVar(this.y) ? test.getVar(this.y) : this.y;
|
||||
|
||||
if (typeof x === "string" || typeof y === "string") {
|
||||
switch (this.op) {
|
||||
case "=":
|
||||
return `${x}` === `${y}`;
|
||||
case "<>":
|
||||
return `${x}` !== `${y}`;
|
||||
}
|
||||
} else {
|
||||
switch (this.op) {
|
||||
case "<":
|
||||
return x < y;
|
||||
case "<=":
|
||||
return x <= y;
|
||||
case ">":
|
||||
return x > y;
|
||||
case ">=":
|
||||
return x >= y;
|
||||
case "=":
|
||||
return x === y;
|
||||
case "<>":
|
||||
return x !== y;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestWhileInstruction extends TestCompoundInstruction {
|
||||
constructor(public readonly condition: Condition) {
|
||||
super();
|
||||
}
|
||||
|
||||
override *steps(test: Test): Generator<TestInstruction> {
|
||||
while (this.condition.check(test)) {
|
||||
yield this;
|
||||
for (const instruction of this.instructions) {
|
||||
yield* instruction.steps(test) as Generator<TestInstruction>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestEchoInstruction implements TestInstruction {
|
||||
constructor(public readonly content: string) {}
|
||||
async do(test: Test) {
|
||||
test.echo(this.content);
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestClearEchoInstruction implements TestInstruction {
|
||||
async do(test: Test) {
|
||||
test.clearEcho();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestLoadROMInstruction implements TestInstruction {
|
||||
constructor(readonly file: string) {}
|
||||
async do(test: Test) {
|
||||
await test.loadROM(this.file);
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestLoadInstruction implements TestInstruction {
|
||||
constructor(readonly file?: string) {}
|
||||
|
||||
async do(test: Test) {
|
||||
await test.load(this.file);
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestCompareToInstruction implements TestInstruction {
|
||||
constructor(readonly file?: string) {}
|
||||
|
||||
async do(test: Test) {
|
||||
if (this.file) {
|
||||
await test.compareTo(this.file);
|
||||
}
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestOutputFileInstruction implements TestInstruction {
|
||||
constructor(readonly file?: string) {}
|
||||
|
||||
async do(test: Test) {
|
||||
if (this.file) {
|
||||
test.outputFile(this.file);
|
||||
}
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestBreakpointInstruction implements TestInstruction {
|
||||
constructor(
|
||||
readonly variable: string,
|
||||
readonly value: number,
|
||||
) {}
|
||||
|
||||
async do(test: Test) {
|
||||
test.addBreakpoint(this.variable, this.value);
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestClearBreakpointsInstruction implements TestInstruction {
|
||||
async do(test: Test) {
|
||||
test.clearBreakpoints();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Output } from "../output.js";
|
||||
import { Action } from "../types.js";
|
||||
import {
|
||||
OutputParams,
|
||||
TestBreakInstruction,
|
||||
TestInstruction,
|
||||
TestStopInstruction,
|
||||
} from "./instruction.js";
|
||||
|
||||
export const DEFAULT_TIME_WIDTH = 7;
|
||||
|
||||
export abstract class Test<IS extends TestInstruction = TestInstruction> {
|
||||
protected readonly instructions: (IS | TestInstruction)[] = [];
|
||||
protected _outputList: Output[] = [];
|
||||
protected _log = "";
|
||||
fs: FileSystem = new FileSystem();
|
||||
protected doEcho?: Action<string>;
|
||||
protected doCompareTo?: Action<string>;
|
||||
protected dir?: string;
|
||||
protected outputFileName?: string;
|
||||
|
||||
constructor(
|
||||
path?: string,
|
||||
doEcho?: Action<string>,
|
||||
doCompareTo?: Action<string>,
|
||||
) {
|
||||
this.doEcho = doEcho;
|
||||
this.doCompareTo = doCompareTo;
|
||||
this.dir = path;
|
||||
}
|
||||
|
||||
setFileSystem(fs: FileSystem): this {
|
||||
this.fs = fs;
|
||||
return this;
|
||||
}
|
||||
|
||||
echo(_content: string) {
|
||||
this.doEcho?.(_content);
|
||||
return;
|
||||
}
|
||||
clearEcho() {
|
||||
this.doEcho?.("");
|
||||
return;
|
||||
}
|
||||
|
||||
async loadROM(_filename?: string): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
async load(_filename?: string): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async compareTo(filename: string): Promise<void> {
|
||||
this.doCompareTo?.(filename);
|
||||
}
|
||||
outputFile(filename: string): void {
|
||||
this.outputFileName = filename;
|
||||
}
|
||||
|
||||
private createOutputs(params: OutputParams[]): Output[] {
|
||||
return params.map((param) => {
|
||||
if (param.len === -1) {
|
||||
if (param.id === "time") {
|
||||
param.len = DEFAULT_TIME_WIDTH;
|
||||
param.style = "S";
|
||||
} else {
|
||||
const width = this.getWidth(param.id, param.address);
|
||||
if (param.style === "B") {
|
||||
param.len = width;
|
||||
} else if (param.style === "D") {
|
||||
param.len = Math.ceil(Math.log(width));
|
||||
} else if (param.style === "X") {
|
||||
param.len = Math.ceil(width / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Output(
|
||||
param.id,
|
||||
param.style,
|
||||
param.len,
|
||||
param.lpad,
|
||||
param.rpad,
|
||||
param.builtin,
|
||||
param.address,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
outputList(params: OutputParams[]): void {
|
||||
this._outputList = this.createOutputs(params);
|
||||
}
|
||||
|
||||
addInstruction(instruction: IS | TestInstruction): void {
|
||||
this.instructions.push(instruction);
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
this._steps = (function* (test) {
|
||||
for (const instruction of test.instructions) {
|
||||
yield* instruction.steps(test);
|
||||
}
|
||||
})(this);
|
||||
this._step = this._steps.next();
|
||||
this._log = "";
|
||||
return this;
|
||||
}
|
||||
|
||||
private _steps!: IterableIterator<IS | TestInstruction>;
|
||||
private _step!: IteratorResult<IS | TestInstruction, IS | TestInstruction>;
|
||||
|
||||
get steps(): Iterator<IS | TestInstruction> {
|
||||
if (this._steps === undefined) {
|
||||
this.reset();
|
||||
this._steps = assertExists(this._steps, "Reset did not initialize steps");
|
||||
this._step = assertExists(this._step, "Reset did not find first step");
|
||||
}
|
||||
return this._steps;
|
||||
}
|
||||
|
||||
get currentStep(): IS | TestInstruction | undefined {
|
||||
return this._step?.value;
|
||||
}
|
||||
|
||||
get done(): boolean {
|
||||
return this._step?.done ?? false;
|
||||
}
|
||||
|
||||
async step() {
|
||||
while (!this._step.done) {
|
||||
await this._step.value.do(this);
|
||||
this._step = this.steps.next();
|
||||
|
||||
if (this._step.value instanceof TestStopInstruction) {
|
||||
this._step = this.steps.next();
|
||||
return false;
|
||||
} else if (this._step.value instanceof TestBreakInstruction) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.reset();
|
||||
while (!(await this.step()));
|
||||
}
|
||||
|
||||
protected readonly breakpoints: Map<string, number> = new Map();
|
||||
addBreakpoint(variable: string, value: number) {
|
||||
this.breakpoints.set(variable, value);
|
||||
}
|
||||
clearBreakpoints() {
|
||||
this.breakpoints.clear();
|
||||
}
|
||||
|
||||
output() {
|
||||
const values = this._outputList.map((output) => output.print(this));
|
||||
this._log += `|${values.join("|")}|\n`;
|
||||
}
|
||||
|
||||
header() {
|
||||
const values = this._outputList.map((output) => output.header(this));
|
||||
this._log += `|${values.join("|")}|\n`;
|
||||
}
|
||||
|
||||
log() {
|
||||
return this._log;
|
||||
}
|
||||
|
||||
abstract hasVar(variable: string | number): boolean;
|
||||
abstract getVar(variable: string | number, offset?: number): number | string;
|
||||
abstract setVar(variable: string, value: number, offset?: number): void;
|
||||
abstract getWidth(variable: string, offset?: number): number;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { VM_PROJECTS } from "@nand2tetris/projects/base.js";
|
||||
import { resetFiles } from "@nand2tetris/projects/full.js";
|
||||
import { TST } from "../languages/tst.js";
|
||||
import { VMTest } from "./vmtst.js";
|
||||
|
||||
async function prepare(project: "07" | "08", name: string): Promise<VMTest> {
|
||||
const fs = new FileSystem(new ObjectFileSystemAdapter({}));
|
||||
await resetFiles(fs);
|
||||
fs.cd(`/projects/${project}/${name}`);
|
||||
const vm_tst = await fs.readFile(name + "VME.tst");
|
||||
const tst = unwrap(TST.parse(vm_tst));
|
||||
const test = unwrap(VMTest.from(tst)).using(fs);
|
||||
await test.load();
|
||||
return test;
|
||||
}
|
||||
|
||||
describe("VM Test Runner", () => {
|
||||
test.each(VM_PROJECTS["07"])("07 VM Test Runner %s", async (name) => {
|
||||
const test = await prepare("07", name);
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await test.step();
|
||||
}
|
||||
});
|
||||
|
||||
test.each(VM_PROJECTS["08"])("08 VM Test Runner %s", async (name) => {
|
||||
const test = await prepare("08", name);
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
test.step();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { RAM } from "../cpu/memory.js";
|
||||
import { Tst } from "../languages/tst.js";
|
||||
import { Segment } from "../languages/vm.js";
|
||||
import { Action, AsyncAction } from "../types.js";
|
||||
import { Vm } from "../vm/vm.js";
|
||||
import { fill } from "./builder.js";
|
||||
import { TestInstruction } from "./instruction.js";
|
||||
import { Test } from "./tst.js";
|
||||
|
||||
export interface VmFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class VMTest extends Test<VMTestInstruction> {
|
||||
vm: Vm = new Vm();
|
||||
|
||||
private doLoad?: AsyncAction<string>;
|
||||
|
||||
static from(
|
||||
tst: Tst,
|
||||
options: {
|
||||
dir?: string;
|
||||
doLoad?: AsyncAction<string>;
|
||||
doEcho?: Action<string>;
|
||||
compareTo?: Action<string>;
|
||||
} = {},
|
||||
): Result<VMTest, Error> {
|
||||
const test = new VMTest(options);
|
||||
return fill(test, tst);
|
||||
}
|
||||
|
||||
constructor({
|
||||
dir,
|
||||
doEcho,
|
||||
doLoad,
|
||||
compareTo,
|
||||
}: {
|
||||
dir?: string;
|
||||
doEcho?: Action<string>;
|
||||
doLoad?: AsyncAction<string>;
|
||||
compareTo?: Action<string>;
|
||||
} = {}) {
|
||||
super(dir, doEcho, compareTo);
|
||||
this.doLoad = doLoad;
|
||||
}
|
||||
|
||||
using(fs: FileSystem): this {
|
||||
this.fs = fs;
|
||||
return this;
|
||||
}
|
||||
|
||||
with(vm: Vm) {
|
||||
this.vm = vm;
|
||||
return this;
|
||||
}
|
||||
|
||||
override async load(filename?: string): Promise<void> {
|
||||
if (!this.dir) return;
|
||||
const dir = assertExists(this.dir?.split("/").slice(0, -1).join("/"));
|
||||
const vm = await this.doLoad?.(filename ? `${dir}/${filename}` : dir);
|
||||
if (vm) {
|
||||
this.vm = vm;
|
||||
}
|
||||
}
|
||||
|
||||
hasVar(variable: string | number, index?: number): boolean {
|
||||
if (typeof variable !== "string") {
|
||||
index = variable;
|
||||
variable = "RAM";
|
||||
}
|
||||
if (
|
||||
variable === "RAM" &&
|
||||
index !== undefined &&
|
||||
index > 0 &&
|
||||
index < RAM.SIZE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
"argument",
|
||||
"local",
|
||||
"static",
|
||||
"constant",
|
||||
"this",
|
||||
"that",
|
||||
"pointer",
|
||||
"temp",
|
||||
].includes(variable.toLowerCase());
|
||||
}
|
||||
|
||||
getVar(variable: string | number, index?: number): number {
|
||||
if (typeof variable !== "string") {
|
||||
index = variable;
|
||||
variable = "RAM";
|
||||
}
|
||||
if (variable === "RAM" && index !== undefined) {
|
||||
return this.vm.RAM.get(index);
|
||||
}
|
||||
return this.vm.memory.getSegment(variable as Segment, index ?? 0);
|
||||
}
|
||||
|
||||
getWidth(variable: string, offset?: number): number {
|
||||
return 16;
|
||||
}
|
||||
|
||||
setVar(variable: string, value: number, index?: number): void {
|
||||
if (typeof variable !== "string") {
|
||||
index = variable;
|
||||
variable = "RAM";
|
||||
}
|
||||
if (variable === "RAM" && index !== undefined) {
|
||||
this.vm.RAM.set(index, value);
|
||||
return;
|
||||
}
|
||||
if (index !== undefined) {
|
||||
this.vm.memory.setSegment(variable as Segment, index, value);
|
||||
} else {
|
||||
switch (variable.toLowerCase()) {
|
||||
case "sp":
|
||||
this.vm.memory.SP = value;
|
||||
break;
|
||||
case "arg":
|
||||
case "argument":
|
||||
this.vm.memory.ARG = value;
|
||||
this.vm.segmentInitializations["argument"].initialized = true;
|
||||
break;
|
||||
case "lcl":
|
||||
case "local":
|
||||
this.vm.memory.LCL = value;
|
||||
this.vm.segmentInitializations["local"].initialized = true;
|
||||
break;
|
||||
case "this":
|
||||
this.vm.memory.THIS = value;
|
||||
this.vm.invocation.thisInitialized = true;
|
||||
break;
|
||||
case "that":
|
||||
this.vm.memory.THAT = value;
|
||||
this.vm.invocation.thatInitialized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vmstep(): void {
|
||||
this.vm.step();
|
||||
}
|
||||
}
|
||||
|
||||
export interface VMTestInstruction extends TestInstruction {
|
||||
_vmTestInstruction_: true;
|
||||
do(test: VMTest): Promise<void>;
|
||||
}
|
||||
|
||||
export class TestVMStepInstruction implements VMTestInstruction {
|
||||
readonly _vmTestInstruction_ = true;
|
||||
async do(test: VMTest) {
|
||||
test.vmstep();
|
||||
}
|
||||
|
||||
*steps() {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { CPU } from "../cpu/cpu.js";
|
||||
import { SCREEN_OFFSET } from "../cpu/memory.js";
|
||||
|
||||
const colorfn = () => (Math.random() * 0xffff) & 0xffff;
|
||||
|
||||
export const TickScreen = (cpu: CPU) => {
|
||||
let row = 0;
|
||||
let col = 0;
|
||||
let color = colorfn();
|
||||
return () => {
|
||||
const index = SCREEN_OFFSET + col + row * 32;
|
||||
cpu.RAM.set(index, color);
|
||||
col += 1;
|
||||
if (col >= 32) {
|
||||
col = 0;
|
||||
row += 1;
|
||||
color = colorfn();
|
||||
if (row >= 256) {
|
||||
row = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const JACK = `
|
||||
R2 = 0;
|
||||
while (true) {
|
||||
R2 = !R2
|
||||
R0 = 32;
|
||||
while (R0-->0) {
|
||||
R1 = 256;
|
||||
while (R1-->0) {
|
||||
SCREEN[R1 * 32 + R1] = R2;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM = `
|
||||
push constant 0 ; pop local 2 ; // R2 = 0;
|
||||
label loop // while (true) {
|
||||
push local 2; not; pop local 2; // R2 = !R2
|
||||
push constant 31 ; pop local 0 ; R0 = 32;
|
||||
label row // while (R0-->0) {
|
||||
push constant 255 ; pop local 1; // R1 = 256;
|
||||
label col // while (R1-->0) {
|
||||
push local 2;
|
||||
push constant SCREEN ; push local 1 ;
|
||||
push local 0; push constant 32;
|
||||
call mul 2 ; add ; add ;
|
||||
pop pointer 1; pop that 0 // SCREEN[R0 * 32 + R1] = R2;
|
||||
push local 1 ; push constant 1; sub ; pop local 1;
|
||||
push R1 ; if-goto col // }
|
||||
push local 0 ; push constant 1; sub ; pop local 0;
|
||||
push R0 ; if-goto row // }
|
||||
goto loop; //}
|
||||
`;
|
||||
|
||||
export const ASM = `
|
||||
@R2 M=0 // R2 = 0
|
||||
(OUTER)
|
||||
@R2 D=M M=!D // R2 = !R2
|
||||
@32 D=A @R0 M=D // R0 = 32
|
||||
(ROW) @R0 D=M @R3 M=D @R0 M=D-1 @ROW_END D;JEQ // while R0 --> 0
|
||||
@256 D=A @R1 M=D // R1 = 256
|
||||
(COL) @R1 D=M @R3 M=D @R1 M=D-1 @COL_END D;JEQ // while R1 --> 0
|
||||
@R5 M=0
|
||||
@32 D=A @R3 M=D // R3 = 32
|
||||
@R1 D=M @R4 M=D // R4 = R1
|
||||
(MUL)
|
||||
@R3 D=M @MUL_END D;JEQ // while R3 > 0
|
||||
@R3 D=M @R5 D=D+M // R5 += R3
|
||||
@R3 M=M-1 // R3 -= 1
|
||||
@MUL 0;JMP
|
||||
(MUL_END) // R5 = 32 * R1
|
||||
@R1 D=M @R5 D=M+D @SCREEN D=A+D @R3 M=D // R3 = R1 + R5 + SCREEN
|
||||
@R2 D=M @R3 M=D // SCREEN + (R1 * 32 + R1) = R2;
|
||||
(COL_END)
|
||||
(ROW_END)
|
||||
(OUTER_END)
|
||||
`;
|
||||
export const HACK = ``;
|
||||
@@ -0,0 +1,72 @@
|
||||
export const JACK = `
|
||||
while (R0 > 0) {
|
||||
R2 = R2 + R1
|
||||
R0 = R0 - 1
|
||||
}`;
|
||||
|
||||
export const VM = `
|
||||
(_loop_start)
|
||||
push constant 0
|
||||
push arg 0
|
||||
eq
|
||||
jump-eq _loop_end
|
||||
|
||||
push arg 1
|
||||
push local 0
|
||||
add
|
||||
pop local 0
|
||||
|
||||
push arg 0
|
||||
push constant 1
|
||||
sub
|
||||
pop arg 0
|
||||
|
||||
jump _loop_start
|
||||
|
||||
(_loop_end)
|
||||
jump loop_end
|
||||
`;
|
||||
|
||||
export const ASM = `
|
||||
@R2
|
||||
M=0
|
||||
(LOOP)
|
||||
@R0
|
||||
D=M
|
||||
@END
|
||||
D;JEQ
|
||||
|
||||
@R1
|
||||
D=M
|
||||
@R2
|
||||
D=D+M
|
||||
M=D
|
||||
|
||||
@R0
|
||||
M=M-1
|
||||
@LOOP
|
||||
0;JMP
|
||||
(END)
|
||||
@END
|
||||
0;JMP
|
||||
`;
|
||||
|
||||
export const HACK = new Int16Array([
|
||||
0x0002, // @R2
|
||||
0xda88, // M=0
|
||||
0x0000, // (LOOP) @R0
|
||||
0xfc10, // D=M
|
||||
0x000f, // @END
|
||||
0xd302, // D;JEQ
|
||||
0x0001, // @R1
|
||||
0xfc10, // D=M
|
||||
0x0002, // @R2
|
||||
0xf090, // D=D+M
|
||||
0xd308, // M=D
|
||||
0x0000, // @R0
|
||||
0xfc88, // M=M-1
|
||||
0x0002, // @LOOP
|
||||
0xda87, // 0;JMP
|
||||
0x000f, // (END) @END
|
||||
0xda87, // 0;JMP
|
||||
]);
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Clock } from "./chip/clock.js";
|
||||
|
||||
export const MAX_STEPS = 1000;
|
||||
|
||||
const clock = Clock.get();
|
||||
|
||||
const BUDGET = 8; // ms allowed per tick
|
||||
|
||||
export abstract class Timer {
|
||||
frame() {
|
||||
this.tick();
|
||||
this.finishFrame();
|
||||
}
|
||||
|
||||
/// Update the simulation state, but DO NOT perform any UI changes.
|
||||
|
||||
// Note: This used to by synchronous for performance reasons,
|
||||
// but it caused a problem where a 'ROM32k load' test instruction would not resolve before the next ones,
|
||||
// causing the Computer chip to run bad instructions and fail the test script
|
||||
abstract tick(): Promise<boolean>;
|
||||
|
||||
/// UI Updates are allowed in finishFrame.
|
||||
finishFrame() {
|
||||
clock.frame();
|
||||
}
|
||||
|
||||
abstract reset(): void;
|
||||
|
||||
abstract toggle(): void;
|
||||
|
||||
_steps = 1; // How many steps to take per update
|
||||
_steps_actual = 1;
|
||||
get steps() {
|
||||
return this._steps;
|
||||
}
|
||||
set steps(value: number) {
|
||||
this._steps = value;
|
||||
this._steps_actual = value;
|
||||
}
|
||||
|
||||
_speed = 60; // how often to update, in ms
|
||||
get speed() {
|
||||
return this._speed;
|
||||
}
|
||||
set speed(value: number) {
|
||||
this._speed = value;
|
||||
}
|
||||
|
||||
get running() {
|
||||
return this.#running;
|
||||
}
|
||||
|
||||
#running = false;
|
||||
#sinceLastFrame = 0;
|
||||
#lastUpdate = 0;
|
||||
#run = async () => {
|
||||
if (!this.#running) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const delta = now - this.#lastUpdate;
|
||||
this.#lastUpdate = now;
|
||||
this.#sinceLastFrame += delta;
|
||||
if (this.#sinceLastFrame > this.speed) {
|
||||
let done = false;
|
||||
let steps = Math.min(this._steps, this._steps_actual);
|
||||
|
||||
const startTime = performance.now();
|
||||
while (!done && steps-- > 0) {
|
||||
done = await this.tick();
|
||||
}
|
||||
const endTime = performance.now();
|
||||
|
||||
// Dynamically adjust steps to stay within BUDGET ms per update, to avoid blocking the main thread.
|
||||
const duration = endTime - startTime;
|
||||
this._steps_actual *= BUDGET / duration;
|
||||
this._steps_actual = Math.ceil(this._steps_actual);
|
||||
|
||||
this.finishFrame();
|
||||
if (done) {
|
||||
this.stop();
|
||||
}
|
||||
this.#sinceLastFrame -= this.speed;
|
||||
}
|
||||
requestAnimationFrame(this.#run);
|
||||
};
|
||||
|
||||
start() {
|
||||
this.#running = true;
|
||||
this.#lastUpdate = Date.now() - this.speed;
|
||||
this.#run();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#running = false;
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type Action<T> = (value: T) => void;
|
||||
export type AsyncAction<T> = (value: T) => Promise<void>;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ASSIGN, COMMANDS, JUMP } from "../cpu/alu.js";
|
||||
import { asm, makeC } from "./asm.js";
|
||||
|
||||
describe("asm", () => {
|
||||
it("converts int16 to asm", () => {
|
||||
expect(asm(0x0000)).toBe("@0");
|
||||
expect(asm(12)).toBe("@12");
|
||||
|
||||
expect(asm(0b1110_101010_000_000)).toBe("0");
|
||||
expect(asm(0b1111_110000_010_000)).toBe("D=M");
|
||||
expect(asm(0b1110_001110_010_101)).toBe("D=D-1;JNE");
|
||||
expect(asm(0b1110_101010_000_111)).toBe("0;JMP");
|
||||
expect(asm(0b1111_110010_011_000)).toBe("MD=M-1");
|
||||
});
|
||||
|
||||
it("makes C instruction", () => {
|
||||
expect(
|
||||
makeC(true, COMMANDS.getOp("D"), ASSIGN.asm["M"], JUMP.asm[""]),
|
||||
).toBe(0b111_1_001100_001_000);
|
||||
expect(
|
||||
makeC(true, COMMANDS.getOp("D-M"), ASSIGN.asm["D"], JUMP.asm[""]),
|
||||
).toBe(0b111_1_010011_010_000);
|
||||
expect(
|
||||
makeC(false, COMMANDS.getOp("D"), ASSIGN.asm[""], JUMP.asm["JGT"]),
|
||||
).toBe(0b111_0_001100_000_001);
|
||||
expect(
|
||||
makeC(false, COMMANDS.getOp("0"), ASSIGN.asm[""], JUMP.asm["JMP"]),
|
||||
).toBe(0b111_0_101010_000_111);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
ASSIGN,
|
||||
ASSIGN_OP,
|
||||
COMMANDS,
|
||||
COMMANDS_ASM,
|
||||
COMMANDS_OP,
|
||||
isAssignAsm,
|
||||
isCommandAsm,
|
||||
isJumpAsm,
|
||||
JUMP,
|
||||
JUMP_OP,
|
||||
} from "../cpu/alu.js";
|
||||
|
||||
export type CommandOps = keyof typeof COMMANDS.op;
|
||||
export type JumpOps = keyof typeof JUMP.op;
|
||||
export type StoreOps = keyof typeof ASSIGN.op;
|
||||
|
||||
export function asm(op: number): string {
|
||||
if (op & 0x8000) {
|
||||
return cInstruction(op);
|
||||
}
|
||||
return aInstruction(op);
|
||||
}
|
||||
|
||||
function cInstruction(op: number): string {
|
||||
op = op & 0xffff; // Clear high order bits
|
||||
const mop = (op & 0x1000) >> 12;
|
||||
const cop: CommandOps = ((op & 0b0000111111000000) >> 6) as CommandOps;
|
||||
const sop: StoreOps = ((op & 0b0000000000111000) >> 3) as StoreOps;
|
||||
const jop: JumpOps = (op & 0b0000000000000111) as JumpOps;
|
||||
|
||||
if (COMMANDS.op[cop] === undefined) {
|
||||
// Invalid commend
|
||||
return "#ERR";
|
||||
}
|
||||
|
||||
let command = COMMANDS.op[cop];
|
||||
if (mop) {
|
||||
command = command.replace(/A/g, "M") as COMMANDS_ASM;
|
||||
}
|
||||
|
||||
const store = ASSIGN.op[sop];
|
||||
const jump = JUMP.op[jop];
|
||||
|
||||
let instruction: string = command;
|
||||
if (store) {
|
||||
instruction = `${store}=${instruction}`;
|
||||
}
|
||||
if (jump) {
|
||||
instruction = `${instruction};${jump}`;
|
||||
}
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
function aInstruction(op: number): string {
|
||||
return "@" + (op & 0x7fff).toString(10);
|
||||
}
|
||||
|
||||
export function op(asm: string): number {
|
||||
if (asm[0] === "@") {
|
||||
return aop(asm);
|
||||
} else {
|
||||
return cop(asm);
|
||||
}
|
||||
}
|
||||
|
||||
function aop(asm: string): number {
|
||||
return parseInt(asm.substring(1), 10);
|
||||
}
|
||||
|
||||
function cop(asm: string): number {
|
||||
const firstPass = asm.match(
|
||||
/(?:(?<assignExists>.+)=)?(.+)(?:;(?<jumpExists>.+))?/,
|
||||
);
|
||||
const { assignExists, jumpExists } = firstPass?.groups ?? {};
|
||||
|
||||
const parts = asm.match(
|
||||
/(?:(?<assign>[AMD]{1,3})=)?(?<operation>[-+!01ADM&|]{1,3})(?:;(?<jump>JGT|JLT|JGE|JLE|JEQ|JMP))?/,
|
||||
);
|
||||
let { assign, jump } = parts?.groups ?? {};
|
||||
const { operation } = parts?.groups ?? {};
|
||||
|
||||
assign = assign ?? (assignExists ? undefined : "");
|
||||
jump = jump ?? (jumpExists ? undefined : "");
|
||||
if (
|
||||
parts?.[0] != asm || // match is not exhaustive
|
||||
!isAssignAsm(assign) ||
|
||||
!isJumpAsm(jump) ||
|
||||
!isCommandAsm(operation)
|
||||
) {
|
||||
// TODO: This should return Result<> instead of throw
|
||||
throw new Error("Invalid c instruction");
|
||||
}
|
||||
|
||||
const mode = operation.includes("M");
|
||||
const aop = ASSIGN.asm[assign];
|
||||
const jop = JUMP.asm[jump];
|
||||
const cop = COMMANDS.getOp(operation);
|
||||
|
||||
return makeC(mode, cop, aop, jop);
|
||||
}
|
||||
|
||||
export function makeC(
|
||||
isM: boolean,
|
||||
op: COMMANDS_OP,
|
||||
assign: ASSIGN_OP,
|
||||
jmp: JUMP_OP,
|
||||
): number {
|
||||
const C = 0xe000;
|
||||
const A = isM ? 0x1000 : 0;
|
||||
const O = op << 6;
|
||||
const D = assign << 3;
|
||||
const J = jmp;
|
||||
return C + A + O + D + J;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { bin, dec, hex, int2, int10, int16, nand16 } from "./twos.js";
|
||||
|
||||
describe("twos", () => {
|
||||
it("formats as base 16", () => {
|
||||
// expect(bin(0)).toBe("0000 0000 0000 0000");
|
||||
// expect(bin(1)).toBe("0000 0000 0000 0001");
|
||||
// expect(bin(-1)).toBe("1111 1111 1111 1111");
|
||||
// expect(bin(256)).toBe("0000 0001 0000 0000");
|
||||
expect(bin(0)).toBe("0000000000000000");
|
||||
expect(bin(1)).toBe("0000000000000001");
|
||||
expect(bin(-1)).toBe("1111111111111111");
|
||||
expect(bin(256)).toBe("0000000100000000");
|
||||
|
||||
expect(bin(6, 4)).toBe("0110");
|
||||
|
||||
expect(dec(0)).toBe("0");
|
||||
expect(dec(1)).toBe("1");
|
||||
expect(dec(-1)).toBe("-1");
|
||||
expect(dec(33413)).toBe("-32123");
|
||||
expect(dec(0x8000)).toBe("-32768");
|
||||
expect(dec(256)).toBe("256");
|
||||
|
||||
expect(hex(0)).toBe("0x0000");
|
||||
expect(hex(1)).toBe("0x0001");
|
||||
expect(hex(-1)).toBe("0xFFFF");
|
||||
expect(hex(256)).toBe("0x0100");
|
||||
});
|
||||
|
||||
it("parses to integer", () => {
|
||||
expect(int2("0000000000000000")).toBe(0);
|
||||
expect(int2("0000000000000001")).toBe(1);
|
||||
expect(int2("1111111111111111")).toBe(65535);
|
||||
expect(int2("0000000100000000")).toBe(256);
|
||||
expect(int2("0000 0000 0000 0000")).toBe(0);
|
||||
expect(int2("0000 0000 0000 0001")).toBe(1);
|
||||
expect(int2("1111 1111 1111 1111")).toBe(65535);
|
||||
expect(int2("0000 0001 0000 0000")).toBe(256);
|
||||
|
||||
expect(int10("0")).toBe(0);
|
||||
expect(int10("1")).toBe(1);
|
||||
expect(int10("-1")).toBe(65535);
|
||||
expect(int10("-32123")).toBe(33413);
|
||||
expect(int10("-32768")).toBe(0x8000);
|
||||
expect(int10("256")).toBe(256);
|
||||
|
||||
expect(int16("0x0000")).toBe(0);
|
||||
expect(int16("0x0001")).toBe(1);
|
||||
expect(int16("0xffff")).toBe(65535);
|
||||
expect(int16("0xFFFF")).toBe(65535);
|
||||
expect(int16("0x0100")).toBe(256);
|
||||
});
|
||||
|
||||
it("nands 16 bit numbers", () => {
|
||||
expect(nand16(0b0, 0b0)).toBe(0b1111_1111_1111_1111);
|
||||
expect(nand16(0b1, 0b0)).toBe(0b1111_1111_1111_1111);
|
||||
expect(nand16(0b0, 0b1)).toBe(0b1111_1111_1111_1111);
|
||||
expect(nand16(0b1, 0b1)).toBe(0b1111_1111_1111_1110);
|
||||
expect(nand16(0b1010_1010_1010_1010, 0b0101_0101_0101_0101)).toBe(
|
||||
0b1111_1111_1111_1111,
|
||||
);
|
||||
expect(nand16(0b1111_0000_1111_0000, 0b1111_0000_0000_1111)).toBe(
|
||||
0b0000_1111_1111_1111,
|
||||
);
|
||||
expect(nand16(0b1111_1111_0000_1111_0000, 0b1111_1111_0000_0000_1111)).toBe(
|
||||
0b0000_1111_1111_1111,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
const Hex = [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
];
|
||||
|
||||
export function chars(i: number): string {
|
||||
return Hex[i] ?? "X";
|
||||
}
|
||||
|
||||
export function bits(i: number): string {
|
||||
switch (i) {
|
||||
case 0x0:
|
||||
return "0000";
|
||||
case 0x1:
|
||||
return "0001";
|
||||
case 0x2:
|
||||
return "0010";
|
||||
case 0x3:
|
||||
return "0011";
|
||||
case 0x4:
|
||||
return "0100";
|
||||
case 0x5:
|
||||
return "0101";
|
||||
case 0x6:
|
||||
return "0110";
|
||||
case 0x7:
|
||||
return "0111";
|
||||
case 0x8:
|
||||
return "1000";
|
||||
case 0x9:
|
||||
return "1001";
|
||||
case 0xa:
|
||||
return "1010";
|
||||
case 0xb:
|
||||
return "1011";
|
||||
case 0xc:
|
||||
return "1100";
|
||||
case 0xd:
|
||||
return "1101";
|
||||
case 0xe:
|
||||
return "1110";
|
||||
case 0xf:
|
||||
return "1111";
|
||||
default:
|
||||
return "erro";
|
||||
}
|
||||
}
|
||||
|
||||
export function int(n: string, radix: number): number {
|
||||
const i = parseInt(n.replace(/[^\d a-f A-F +-.]/g, ""), radix);
|
||||
return i & 0xffff;
|
||||
}
|
||||
|
||||
export function int16(i: string): number {
|
||||
return int(i, 16);
|
||||
}
|
||||
|
||||
export function int10(i: string): number {
|
||||
return int(i, 10);
|
||||
}
|
||||
|
||||
export function int2(i: string): number {
|
||||
return int(i.replaceAll(" ", ""), 2);
|
||||
}
|
||||
|
||||
export function parseTwosInt(i: string): number {
|
||||
if (i.toUpperCase().includes("X")) {
|
||||
return int16(i);
|
||||
}
|
||||
return int10(i);
|
||||
}
|
||||
|
||||
export function hex(i: number): string {
|
||||
const hu = chars((i & 0xf000) >> 12);
|
||||
const hl = chars((i & 0x0f00) >> 8);
|
||||
const lu = chars((i & 0x00f0) >> 4);
|
||||
const ll = chars(i & 0x000f);
|
||||
|
||||
return `0x${hu}${hl}${lu}${ll}`;
|
||||
}
|
||||
|
||||
export function bin(i: number, precision = 16): string {
|
||||
const hu = bits((i & 0xf000) >> 12);
|
||||
const hl = bits((i & 0x0f00) >> 8);
|
||||
const lu = bits((i & 0x00f0) >> 4);
|
||||
const ll = bits(i & 0x000f);
|
||||
|
||||
// return `${hu} ${hl} ${lu} ${ll}`;
|
||||
return `${hu}${hl}${lu}${ll}`.substring(16 - precision); // Match the book's formatting
|
||||
}
|
||||
|
||||
export function dec(i: number): string {
|
||||
i = i & 0xffff;
|
||||
if (i === 0x8000) {
|
||||
return "-32768";
|
||||
}
|
||||
if (i & 0x8000) {
|
||||
i = (~i + 1) & 0x7fff;
|
||||
return `-${i}`;
|
||||
}
|
||||
return `${i}`;
|
||||
}
|
||||
|
||||
export function unsigned(i: number): string {
|
||||
i = i & 0xffff;
|
||||
return `${i}`;
|
||||
}
|
||||
|
||||
export function nand16(a: number, b: number): number {
|
||||
a = a & 0xffff;
|
||||
b = b & 0xffff;
|
||||
let c = ~(a & b);
|
||||
c = c & 0xffff;
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { Vm } from "./vm.js";
|
||||
|
||||
describe("builtins", () => {
|
||||
describe("Math", () => {
|
||||
test("multiply", () => {
|
||||
const vm = unwrap(
|
||||
Vm.build([
|
||||
{ op: "push", segment: "constant", offset: 7 },
|
||||
{ op: "push", segment: "constant", offset: 8 },
|
||||
{ op: "call", name: "Math.multiply", nArgs: 2 },
|
||||
]),
|
||||
);
|
||||
|
||||
vm.step();
|
||||
vm.step();
|
||||
vm.step();
|
||||
expect(vm.read([0, 256])).toEqual([257, 56]);
|
||||
});
|
||||
test("divide", () => {
|
||||
const vm = unwrap(
|
||||
Vm.build([
|
||||
{ op: "push", segment: "constant", offset: 10 },
|
||||
{ op: "push", segment: "constant", offset: 5 },
|
||||
{ op: "call", name: "Math.divide", nArgs: 2 },
|
||||
]),
|
||||
);
|
||||
|
||||
vm.step();
|
||||
vm.step();
|
||||
vm.step();
|
||||
expect(vm.read([0, 256])).toEqual([257, 2]);
|
||||
});
|
||||
test("min", () => {
|
||||
const vm = unwrap(
|
||||
Vm.build([
|
||||
{ op: "push", segment: "constant", offset: 11 },
|
||||
{ op: "push", segment: "constant", offset: 4 },
|
||||
{ op: "call", name: "Math.min", nArgs: 2 },
|
||||
]),
|
||||
);
|
||||
|
||||
vm.step();
|
||||
vm.step();
|
||||
vm.step();
|
||||
expect(vm.read([0, 256])).toEqual([257, 4]);
|
||||
});
|
||||
test("max", () => {
|
||||
const vm = unwrap(
|
||||
Vm.build([
|
||||
{ op: "push", segment: "constant", offset: 11 },
|
||||
{ op: "push", segment: "constant", offset: 4 },
|
||||
{ op: "call", name: "Math.max", nArgs: 2 },
|
||||
]),
|
||||
);
|
||||
|
||||
vm.step();
|
||||
vm.step();
|
||||
vm.step();
|
||||
expect(vm.read([0, 256])).toEqual([257, 11]);
|
||||
});
|
||||
test("sqrt", () => {
|
||||
const vm = unwrap(
|
||||
Vm.build([
|
||||
{ op: "push", segment: "constant", offset: 36 },
|
||||
{ op: "call", name: "Math.sqrt", nArgs: 1 },
|
||||
]),
|
||||
);
|
||||
|
||||
vm.step();
|
||||
vm.step();
|
||||
expect(vm.read([0, 256])).toEqual([257, 6]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,504 @@
|
||||
import {
|
||||
ReturnType,
|
||||
Subroutine,
|
||||
SubroutineType,
|
||||
Type,
|
||||
} from "../languages/jack.js";
|
||||
import { VmMemory } from "./memory.js";
|
||||
import { ERRNO } from "./os/errors.js";
|
||||
import { OS } from "./os/os.js";
|
||||
import { BACKSPACE, DOUBLE_QUOTES, NEW_LINE } from "./os/string.js";
|
||||
|
||||
export type VmBuiltinFunction = (memory: VmMemory, os: OS) => number;
|
||||
|
||||
export interface VmBuiltin {
|
||||
func: VmBuiltinFunction;
|
||||
type: SubroutineType;
|
||||
args: Type[];
|
||||
returnType: ReturnType;
|
||||
}
|
||||
|
||||
function getArgs(memory: VmMemory, n: number) {
|
||||
const args = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
args.push(memory.get(memory.SP - n + i));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
export function overridesOsCorrectly(cls: string, subroutine: Subroutine) {
|
||||
const builtin = VM_BUILTINS[`${cls}.${subroutine.name.value}`];
|
||||
|
||||
return (
|
||||
builtin &&
|
||||
builtin.args.length == subroutine.parameters.length &&
|
||||
builtin.args.every(
|
||||
(arg, index) => arg == subroutine.parameters[index].type.value,
|
||||
) &&
|
||||
builtin.returnType == subroutine.returnType.value
|
||||
);
|
||||
}
|
||||
|
||||
export function makeInterface(name: string, builtin: VmBuiltin) {
|
||||
return `${builtin.returnType} ${name}(${builtin.args.join(",")}`;
|
||||
}
|
||||
|
||||
export const VM_BUILTINS: Record<string, VmBuiltin> = {
|
||||
"Math.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Math.multiply": {
|
||||
func: (memory, _) => {
|
||||
const [a, b] = getArgs(memory, 2);
|
||||
return (a * b) & 0xffff;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Math.divide": {
|
||||
func: (memory, os) => {
|
||||
const [a, b] = getArgs(memory, 2);
|
||||
if (b == 0) {
|
||||
os.sys.error(ERRNO.DIVIDE_BY_ZERO);
|
||||
return 0;
|
||||
}
|
||||
return Math.floor(a / b) & 0xffff;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Math.min": {
|
||||
func: (memory, _) => {
|
||||
const [a, b] = getArgs(memory, 2);
|
||||
return Math.min(a, b) & 0xffff;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Math.max": {
|
||||
func: (memory, _) => {
|
||||
const [a, b] = getArgs(memory, 2);
|
||||
return Math.max(a, b) & 0xffff;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Math.sqrt": {
|
||||
func: (memory, os) => {
|
||||
const [x] = getArgs(memory, 1);
|
||||
if (x < 0) {
|
||||
os.sys.error(ERRNO.SQRT_NEG);
|
||||
return 0;
|
||||
}
|
||||
return Math.floor(Math.sqrt(x)) & 0xffff;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Math.abs": {
|
||||
func: (memory, _) => {
|
||||
const [x] = getArgs(memory, 1);
|
||||
return Math.abs(x) & 0xffff;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.clearScreen": {
|
||||
func: (_, os) => {
|
||||
os.screen.clear();
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.setColor": {
|
||||
func: (memory, os) => {
|
||||
const [color] = getArgs(memory, 1);
|
||||
os.screen.color = color !== 0;
|
||||
return 0;
|
||||
},
|
||||
args: ["boolean"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.drawPixel": {
|
||||
func: (memory, os) => {
|
||||
const [x, y] = getArgs(memory, 2);
|
||||
os.screen.drawPixel(x, y);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.drawLine": {
|
||||
func: (memory, os) => {
|
||||
const [x1, y1, x2, y2] = getArgs(memory, 4);
|
||||
os.screen.drawLine(x1, y1, x2, y2);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int", "int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.drawRectangle": {
|
||||
func: (memory, os) => {
|
||||
const [x1, y1, x2, y2] = getArgs(memory, 4);
|
||||
os.screen.drawRect(x1, y1, x2, y2);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int", "int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Screen.drawCircle": {
|
||||
func: (memory, os) => {
|
||||
const [x, y, r] = getArgs(memory, 3);
|
||||
os.screen.drawCircle(x, y, r);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Memory.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Memory.peek": {
|
||||
func: (memory, _) => {
|
||||
const [address] = getArgs(memory, 1);
|
||||
return memory.get(address);
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Memory.poke": {
|
||||
func: (memory, _) => {
|
||||
const [address, value] = getArgs(memory, 2);
|
||||
memory.set(address, value);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Memory.alloc": {
|
||||
func: (memory, os) => {
|
||||
const [size] = getArgs(memory, 1);
|
||||
return os.memory.alloc(size);
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "Array",
|
||||
type: "function",
|
||||
},
|
||||
"Memory.deAlloc": {
|
||||
func: (memory, os) => {
|
||||
const [address] = getArgs(memory, 1);
|
||||
os.memory.deAlloc(address);
|
||||
return 0;
|
||||
},
|
||||
args: ["Array"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Array.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Array.new": {
|
||||
func: (memory, os) => {
|
||||
const [size] = getArgs(memory, 1);
|
||||
if (size <= 0) {
|
||||
os.sys.error(ERRNO.ARRAY_SIZE_NOT_POSITIVE);
|
||||
return 0;
|
||||
}
|
||||
return os.memory.alloc(size);
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "Array",
|
||||
type: "constructor",
|
||||
},
|
||||
"Array.dispose": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
os.memory.deAlloc(pointer);
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "method",
|
||||
},
|
||||
"String.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"String.new": {
|
||||
func: (memory, os) => {
|
||||
const [length] = getArgs(memory, 1);
|
||||
return os.string.new(length);
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "String",
|
||||
type: "constructor",
|
||||
},
|
||||
"String.dispose": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
os.string.dispose(pointer);
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "method",
|
||||
},
|
||||
"String.length": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
return os.string.length(pointer);
|
||||
},
|
||||
args: [],
|
||||
returnType: "int",
|
||||
type: "method",
|
||||
},
|
||||
"String.charAt": {
|
||||
func: (memory, os) => {
|
||||
const [pointer, index] = getArgs(memory, 2);
|
||||
return os.string.charAt(pointer, index);
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "char",
|
||||
type: "method",
|
||||
},
|
||||
"String.setCharAt": {
|
||||
func: (memory, os) => {
|
||||
const [pointer, index, value] = getArgs(memory, 3);
|
||||
os.string.setCharAt(pointer, index, value);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "char"],
|
||||
returnType: "void",
|
||||
type: "method",
|
||||
},
|
||||
"String.appendChar": {
|
||||
func: (memory, os) => {
|
||||
const [pointer, value] = getArgs(memory, 2);
|
||||
return os.string.appendChar(pointer, value);
|
||||
},
|
||||
args: ["char"],
|
||||
returnType: "String",
|
||||
type: "method",
|
||||
},
|
||||
"String.eraseLastChar": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
os.string.eraseLastChar(pointer);
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "method",
|
||||
},
|
||||
"String.intValue": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
return os.string.intValue(pointer);
|
||||
},
|
||||
args: [],
|
||||
returnType: "int",
|
||||
type: "method",
|
||||
},
|
||||
"String.setInt": {
|
||||
func: (memory, os) => {
|
||||
const [pointer, value] = getArgs(memory, 2);
|
||||
os.string.setInt(pointer, value);
|
||||
return 0;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "void",
|
||||
type: "method",
|
||||
},
|
||||
"String.backSpace": {
|
||||
func: (_, __) => {
|
||||
return BACKSPACE;
|
||||
},
|
||||
args: [],
|
||||
returnType: "char",
|
||||
type: "function",
|
||||
},
|
||||
"String.doubleQuote": {
|
||||
func: (_, __) => {
|
||||
return DOUBLE_QUOTES;
|
||||
},
|
||||
args: [],
|
||||
returnType: "char",
|
||||
type: "function",
|
||||
},
|
||||
"String.newLine": {
|
||||
func: (_, __) => {
|
||||
return NEW_LINE;
|
||||
},
|
||||
args: [],
|
||||
returnType: "char",
|
||||
type: "function",
|
||||
},
|
||||
"Output.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.moveCursor": {
|
||||
func: (memory, os) => {
|
||||
const [i, j] = getArgs(memory, 2);
|
||||
os.output.moveCursor(i, j);
|
||||
return 0;
|
||||
},
|
||||
args: ["int", "int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.printChar": {
|
||||
func: (memory, os) => {
|
||||
const [code] = getArgs(memory, 1);
|
||||
os.output.printChar(code);
|
||||
return 0;
|
||||
},
|
||||
args: ["char"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.printString": {
|
||||
func: (memory, os) => {
|
||||
const [pointer] = getArgs(memory, 1);
|
||||
os.output.printString(pointer);
|
||||
return 0;
|
||||
},
|
||||
args: ["String"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.printInt": {
|
||||
func: (memory, os) => {
|
||||
const [value] = getArgs(memory, 1);
|
||||
os.output.printInt(value);
|
||||
return 0;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.println": {
|
||||
func: (_, os) => {
|
||||
os.output.println();
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Output.backSpace": {
|
||||
func: (_, os) => {
|
||||
os.output.backspace();
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Keyboard.init": {
|
||||
func: (_, __) => 0,
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Keyboard.keyPressed": {
|
||||
func: (_, os) => {
|
||||
return os.keyboard.keyPressed();
|
||||
},
|
||||
args: [],
|
||||
returnType: "char",
|
||||
type: "function",
|
||||
},
|
||||
"Keyboard.readChar": {
|
||||
func: (_, os) => {
|
||||
os.keyboard.readChar();
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "char",
|
||||
type: "function",
|
||||
},
|
||||
"Keyboard.readLine": {
|
||||
func: (memory, os) => {
|
||||
const [message] = getArgs(memory, 1);
|
||||
os.keyboard.readLine(message);
|
||||
return 0;
|
||||
},
|
||||
args: ["String"],
|
||||
returnType: "String",
|
||||
type: "function",
|
||||
},
|
||||
"Keyboard.readInt": {
|
||||
func: (memory, os) => {
|
||||
const [message] = getArgs(memory, 1);
|
||||
os.keyboard.readInt(message);
|
||||
return 0;
|
||||
},
|
||||
args: ["String"],
|
||||
returnType: "int",
|
||||
type: "function",
|
||||
},
|
||||
"Sys.halt": {
|
||||
func: (_, os) => {
|
||||
os.sys.halt();
|
||||
return 0;
|
||||
},
|
||||
args: [],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Sys.error": {
|
||||
func: (memory, os) => {
|
||||
const [code] = getArgs(memory, 1);
|
||||
os.sys.error(code);
|
||||
return 0;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
"Sys.wait": {
|
||||
func: (memory, os) => {
|
||||
const [ms] = getArgs(memory, 1);
|
||||
os.sys.wait(ms);
|
||||
return 0;
|
||||
},
|
||||
args: ["int"],
|
||||
returnType: "void",
|
||||
type: "function",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { RAM } from "../cpu/memory.js";
|
||||
import { Segment } from "../languages/vm.js";
|
||||
import { VmFrame } from "./vm.js";
|
||||
|
||||
export const SP = 0;
|
||||
export const LCL = 1;
|
||||
export const ARG = 2;
|
||||
export const THIS = 3;
|
||||
export const THAT = 4;
|
||||
export const TEMP = 5;
|
||||
export const STATIC = 16;
|
||||
|
||||
export class VmMemory extends RAM {
|
||||
strict = true;
|
||||
get SP(): number {
|
||||
return this.get(SP);
|
||||
}
|
||||
set SP(value: number) {
|
||||
this.set(SP, value);
|
||||
}
|
||||
get LCL(): number {
|
||||
return this.get(LCL);
|
||||
}
|
||||
set LCL(value: number) {
|
||||
this.set(LCL, value);
|
||||
}
|
||||
get ARG(): number {
|
||||
return this.get(ARG);
|
||||
}
|
||||
set ARG(value: number) {
|
||||
this.set(ARG, value);
|
||||
}
|
||||
get THIS(): number {
|
||||
return this.get(THIS);
|
||||
}
|
||||
set THIS(value: number) {
|
||||
this.set(THIS, value);
|
||||
}
|
||||
get THAT(): number {
|
||||
return this.get(THAT);
|
||||
}
|
||||
set THAT(value: number) {
|
||||
this.set(THAT, value);
|
||||
}
|
||||
|
||||
get statics() {
|
||||
const statics = [];
|
||||
for (let i = 16; i < 256; i++) {
|
||||
statics.push(this.get(i));
|
||||
}
|
||||
return statics;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.set(SP, 256);
|
||||
}
|
||||
|
||||
baseSegment(segment: Segment, offset: number): Result<number, Error> {
|
||||
if (this.strict && (offset < 0 || offset > 32767))
|
||||
return Err(
|
||||
new Error(
|
||||
`Illegal offset value ${offset} (must be between 0 and 32767)`,
|
||||
),
|
||||
);
|
||||
switch (segment) {
|
||||
case "argument":
|
||||
return Ok(this.ARG + offset);
|
||||
case "constant":
|
||||
return Ok(offset);
|
||||
case "local":
|
||||
return Ok(this.LCL + offset);
|
||||
case "pointer":
|
||||
if (this.strict && offset > 1)
|
||||
throw new Error(
|
||||
`pointer out of bounds access (pointer can be 0 for this, 1 for that, but got ${offset}`,
|
||||
);
|
||||
return Ok(offset === 0 ? THIS : THAT);
|
||||
case "static":
|
||||
if (this.strict && offset > 255 - 16)
|
||||
return Err(new Error(`Cannot access statics beyond 239: ${offset}`));
|
||||
return Ok(16 + offset);
|
||||
case "temp":
|
||||
if (this.strict && offset > 7)
|
||||
return Err(
|
||||
new Error(
|
||||
`Temp out of bounds access (temp can be 0 to 7, but got ${offset}`,
|
||||
),
|
||||
);
|
||||
return Ok(5 + offset);
|
||||
case "that":
|
||||
return Ok(this.THAT + offset);
|
||||
case "this":
|
||||
return Ok(this.THIS + offset);
|
||||
}
|
||||
}
|
||||
|
||||
getSegment(segment: Segment, offset: number): number {
|
||||
if (segment === "constant") {
|
||||
if (this.strict && (offset < 0 || offset > 32767))
|
||||
throw new Error(
|
||||
`Illegal offset value ${offset} (must be between 0 and 32767)`,
|
||||
);
|
||||
return offset;
|
||||
}
|
||||
const base = this.baseSegment(segment, offset);
|
||||
if (isErr(base)) {
|
||||
throw Err(base);
|
||||
}
|
||||
return this.get(Ok(base));
|
||||
}
|
||||
setSegment(segment: Segment, offset: number, value: number) {
|
||||
const base = this.baseSegment(segment, offset);
|
||||
if (isErr(base)) {
|
||||
throw Err(base);
|
||||
}
|
||||
this.set(Ok(base), value);
|
||||
}
|
||||
|
||||
argument(offset: number): number {
|
||||
return this.getSegment("argument", offset);
|
||||
}
|
||||
local(offset: number): number {
|
||||
return this.getSegment("local", offset);
|
||||
}
|
||||
static(offset: number): number {
|
||||
return this.getSegment("static", offset);
|
||||
}
|
||||
constant(offset: number): number {
|
||||
return this.getSegment("constant", offset);
|
||||
}
|
||||
this(offset: number): number {
|
||||
return this.getSegment("this", offset);
|
||||
}
|
||||
that(offset: number): number {
|
||||
return this.getSegment("that", offset);
|
||||
}
|
||||
pointer(offset: number): number {
|
||||
return this.getSegment("pointer", offset);
|
||||
}
|
||||
temp(offset: number): number {
|
||||
return this.getSegment("temp", offset);
|
||||
}
|
||||
|
||||
push(value: number) {
|
||||
const sp = this.SP;
|
||||
this.set(sp, value);
|
||||
this.set(0, sp + 1);
|
||||
}
|
||||
pop(): number {
|
||||
if (this.strict && this.SP === 256)
|
||||
throw new Error(`Cannot pop the stack below 256 in strict mode`);
|
||||
this.set(0, this.SP - 1);
|
||||
const value = this.get(this.SP);
|
||||
return value;
|
||||
}
|
||||
// Stack frame, from figure 8.3, is:
|
||||
// [ARG] Arg0 Arg1... RET LCL ARG THIS THAT [LCL] Local0 Local1... [SP]
|
||||
pushFrame(ret: number, nArgs: number, nLocals: number): number {
|
||||
const base = this.SP;
|
||||
const arg = base - nArgs;
|
||||
this.set(base, ret);
|
||||
this.set(base + 1, this.LCL);
|
||||
this.set(base + 2, this.ARG);
|
||||
this.set(base + 3, this.THIS);
|
||||
this.set(base + 4, this.THAT);
|
||||
|
||||
this.set(ARG, arg);
|
||||
this.set(LCL, base + 5);
|
||||
|
||||
const sp = base + 5;
|
||||
// Technically this happens in the function, but the VM will handle it for free
|
||||
for (let i = 0; i < nLocals; i++) {
|
||||
this.set(sp + i, 0);
|
||||
}
|
||||
this.set(SP, sp + nLocals);
|
||||
return base;
|
||||
}
|
||||
|
||||
popFrame(): number {
|
||||
const frame = this.LCL;
|
||||
const ret = this.get(frame - 5);
|
||||
const value = this.pop();
|
||||
this.set(this.ARG, value);
|
||||
this.set(SP, this.ARG + 1);
|
||||
this.set(THAT, this.get(frame - 1));
|
||||
this.set(THIS, this.get(frame - 2));
|
||||
this.set(ARG, this.get(frame - 3));
|
||||
this.set(LCL, this.get(frame - 4));
|
||||
return ret;
|
||||
}
|
||||
|
||||
getFrame(
|
||||
base: number, // The address of the frame, the RET address
|
||||
argN: number, // The number of arguments to this frame
|
||||
localN: number, // The number of locals in this frame
|
||||
thisN: number, // The number of items in `this`
|
||||
thatN: number, // the number of items in `that`
|
||||
nextFrame: number,
|
||||
): VmFrame {
|
||||
const arg = base - argN;
|
||||
const lcl = base + 5;
|
||||
const stk = lcl + localN;
|
||||
const stackN = nextFrame - stk;
|
||||
const args = [...this.map((_, v) => v, arg, arg + argN)];
|
||||
const locals = [...this.map((_, v) => v, lcl, lcl + localN)];
|
||||
const stack = [...this.map((_, v) => v, stk, stk + stackN)];
|
||||
const this_ = [...this.map((_, v) => v, this.THIS, this.THIS + thisN)];
|
||||
const that = [...this.map((_, v) => v, this.THAT, this.THAT + thatN)];
|
||||
return {
|
||||
args: { base: arg, count: argN, values: args },
|
||||
locals: { base: lcl, count: localN, values: locals },
|
||||
stack: { base: stk, count: stackN, values: stack },
|
||||
this: { base: stk, count: thisN, values: this_ },
|
||||
that: { base: stk, count: thatN, values: that },
|
||||
frame: {
|
||||
RET: this.get(base),
|
||||
LCL: this.LCL,
|
||||
ARG: this.ARG,
|
||||
THIS: this.THIS,
|
||||
THAT: this.THAT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getVmState(staticN = 240) {
|
||||
const temps = [...this.map((_, v) => v, 5, 13)];
|
||||
const internal = [...this.map((_, v) => v, 13, 16)];
|
||||
const statics = [...this.map((_, v) => v, 16, 16 + staticN)];
|
||||
return {
|
||||
["0: SP"]: this.SP,
|
||||
["1: LCL"]: this.LCL,
|
||||
["2: ARG"]: this.ARG,
|
||||
["3: THIS"]: this.THIS,
|
||||
["4: THAT"]: this.THAT,
|
||||
temps,
|
||||
internal,
|
||||
static: statics,
|
||||
};
|
||||
}
|
||||
|
||||
binOp(fn: (a: number, b: number) => number) {
|
||||
const a = this.get(this.SP - 2);
|
||||
const b = this.get(this.SP - 1);
|
||||
const v = fn(a, b) & 0xffff;
|
||||
this.set(this.SP - 2, v);
|
||||
this.set(SP, this.SP - 1);
|
||||
}
|
||||
unOp(fn: (a: number) => number) {
|
||||
const a = this.get(this.SP - 1);
|
||||
const v = fn(a) & 0xffff;
|
||||
this.set(this.SP - 1, v);
|
||||
}
|
||||
comp(fn: (a: number, b: number) => boolean) {
|
||||
this.binOp((a, b) => (fn(a, b) ? -1 : 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export enum ERRNO {
|
||||
SYS_WAIT_DURATION_NOT_POSITIVE = 1,
|
||||
ARRAY_SIZE_NOT_POSITIVE = 2,
|
||||
DIVIDE_BY_ZERO = 3,
|
||||
SQRT_NEG = 4,
|
||||
ALLOC_SIZE_NOT_POSITIVE = 5,
|
||||
HEAP_OVERFLOW = 6,
|
||||
ILLEGAL_PIXEL_COORD = 7,
|
||||
ILLEGAL_LINE_COORD = 8,
|
||||
ILLEGAL_RECT_COORD = 9,
|
||||
ILLEGAL_CENTER_COORD = 12,
|
||||
ILLEGAL_RADIUS = 13,
|
||||
STRING_LENGTH_NEG = 14,
|
||||
GET_CHAR_INDEX_OUT_OF_BOUNDS = 15,
|
||||
SET_CHAR_INDEX_OUT_OF_BOUNDS = 16,
|
||||
STRING_FULL = 17,
|
||||
STRING_EMPTY = 18,
|
||||
STRING_INSUFFICIENT_CAPACITY = 19,
|
||||
ILLEGAL_CURSOR_LOCATION = 20,
|
||||
}
|
||||
|
||||
export function isSysError(errno: number): errno is ERRNO {
|
||||
return Object.values(ERRNO).includes(errno as ERRNO);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user