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
@@ -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");