Web-Ide mit aufgenommen

This commit is contained in:
Riwoldt
2026-04-09 14:14:56 +02:00
parent 64816c45cc
commit 15cfaf332d
489 changed files with 186891 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};
+11
View File
@@ -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/.*)"],
};
+39
View File
@@ -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"
}
}
+1
View File
@@ -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);
}`;
+532
View File
@@ -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);
});
});
+830
View File
@@ -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",
];
+92
View File
@@ -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(
{},
);
});
});
+82
View File
@@ -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)
});
});
+330
View File
@@ -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];
}
+202
View File
@@ -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);
});
});
+221
View File
@@ -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;
}
}
+234
View File
@@ -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);
}
}
+25
View File
@@ -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");
});
});
+651
View File
@@ -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);
});
});
+365
View 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);
});
});
});
+154
View File
@@ -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();
});
+272
View File
@@ -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);
});
+260
View File
@@ -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),
};
+30
View File
@@ -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);
}
+115
View File
@@ -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]");
});
});
+109
View File
@@ -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)),
);
}
+101
View File
@@ -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",
};
},
});
+154
View File
@@ -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();
});
});
+160
View File
@@ -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;
}
}
+187
View File
@@ -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;
}
}
+176
View File
@@ -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();
}
});
});
+167
View File
@@ -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
]);
+99
View File
@@ -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();
}
}
+2
View File
@@ -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);
});
});
+116
View File
@@ -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,
);
});
});
+129
View File
@@ -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]);
});
});
});
+504
View File
@@ -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",
},
};
+263
View File
@@ -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