Web-Ide mit aufgenommen
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { HDL } from "../languages/hdl.js";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { build, parse } from "./builder.js";
|
||||
import { Chip, HIGH, LOW } from "./chip.js";
|
||||
|
||||
function asDisplay(e: unknown): string {
|
||||
return display(
|
||||
(e as { message: string }).message ??
|
||||
(e as { shortMessage: string }).shortMessage ??
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Chip Builder", () => {
|
||||
it("builds a chip from a string", async () => {
|
||||
const nand = unwrap(
|
||||
await parse(
|
||||
`CHIP Not { IN in; OUT out; PARTS: Nand(a=in, b=in, out=out); }`,
|
||||
),
|
||||
);
|
||||
|
||||
nand.in().pull(LOW);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in().pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(LOW);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subbus components", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(
|
||||
`CHIP Foo {
|
||||
IN six[3];
|
||||
OUT out;
|
||||
PARTS: Not16(
|
||||
in[0..1] = true,
|
||||
in[3..5] = six,
|
||||
in[7] = true,
|
||||
);
|
||||
}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
const six = foo.in("six");
|
||||
six.busVoltage = 6;
|
||||
foo.eval();
|
||||
const inVoltage = [...foo.parts][0].in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b10110011));
|
||||
|
||||
// const outVoltage = foo.pin("out1").busVoltage;
|
||||
// expect(outVoltage).toBe(0b01001);
|
||||
// expect(outVoltage).toBe(0b11001);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subpins", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(`
|
||||
CHIP Not2 {
|
||||
IN in[2];
|
||||
OUT out[2];
|
||||
PARTS:
|
||||
Not(in=in[0], out=out[0]);
|
||||
Not(in=in[1], out=out[1]);
|
||||
}
|
||||
`),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in().busVoltage = 0b00;
|
||||
foo.eval();
|
||||
expect(foo.out().busVoltage).toBe(0b11);
|
||||
|
||||
foo.in().busVoltage = 0b11;
|
||||
foo.eval();
|
||||
expect(foo.out().busVoltage).toBe(0b00);
|
||||
});
|
||||
|
||||
it("builds and evals a chip with subbus components on the right", async () => {
|
||||
let foo: Chip;
|
||||
try {
|
||||
foo = unwrap(
|
||||
await parse(
|
||||
`CHIP Foo {
|
||||
IN in[16];
|
||||
OUT out[5];
|
||||
PARTS: Not16(
|
||||
in[0..7] = in[4..11],
|
||||
// in[8..15] = false,
|
||||
out[3..5] = out[1..3],
|
||||
);
|
||||
}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in().busVoltage = 0b1010_1100_0011_0101;
|
||||
foo.eval();
|
||||
const inVoltage = [...foo.parts][0].in().busVoltage;
|
||||
const outVoltage = foo.out().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b11000011));
|
||||
expect(bin(outVoltage)).toBe(bin(0b01110));
|
||||
});
|
||||
|
||||
it("looks up unknown chips in fs", async () => {
|
||||
const fs = new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "Copy.hdl": COPY_HDL }),
|
||||
);
|
||||
|
||||
let foo: Chip;
|
||||
|
||||
try {
|
||||
const chip = unwrap(await HDL.parse(USE_COPY_HDL));
|
||||
foo = unwrap(await build({ parts: chip, dir: ".", fs }));
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
|
||||
foo.in("a").pull(HIGH);
|
||||
foo.eval();
|
||||
expect(foo.out("b").busVoltage).toBe(1);
|
||||
|
||||
foo.in("a").pull(LOW);
|
||||
foo.eval();
|
||||
expect(foo.out("b").busVoltage).toBe(0);
|
||||
});
|
||||
|
||||
it("returns error for mismatching input width", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Foo {
|
||||
IN in[3]; OUT out;
|
||||
PARTS: Or8Way(in=in, out=out);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for mismatching output width", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Foo {
|
||||
IN in; OUT out[5];
|
||||
PARTS: Not(in=in, out=out);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for wire loop", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
Nand(a=in, b=myNand, out=myNand);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns error for part loop", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Not {
|
||||
IN in;
|
||||
OUT out;
|
||||
PARTS:
|
||||
Nand(a=in, b=b, out=c);
|
||||
Nand(a=in, b=c, out=b);
|
||||
}`),
|
||||
);
|
||||
const foo = await build({ parts: chip });
|
||||
expect(foo).toBeErr();
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
|
||||
it("sorts after wiring", async () => {
|
||||
try {
|
||||
const chip = unwrap(
|
||||
HDL.parse(`CHIP Or { IN a, b; OUT out;
|
||||
PARTS:
|
||||
Not(in =b , out = net2);
|
||||
Nand(a = net, b =net2 , out =out );
|
||||
Not(in =a , out = net);
|
||||
}`),
|
||||
);
|
||||
const orA = await build({ parts: chip });
|
||||
expect(orA).toBeOk();
|
||||
|
||||
const ora = unwrap(orA);
|
||||
|
||||
ora.in("a").pull(HIGH);
|
||||
ora.in("b").pull(LOW);
|
||||
ora.eval();
|
||||
expect(ora.out("out").busVoltage).toBe(HIGH);
|
||||
} catch (e) {
|
||||
throw new Error(asDisplay(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const USE_COPY_HDL = `CHIP UseCopy {
|
||||
IN a; OUT b;
|
||||
PARTS: Copy(in=a, out=b);
|
||||
}`;
|
||||
|
||||
const COPY_HDL = `CHIP Copy {
|
||||
IN in; OUT out;
|
||||
PARTS: Or(a=in, b=in, out=out);
|
||||
}`;
|
||||
@@ -0,0 +1,532 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
isOk,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CompilationError, createError, Span } from "../languages/base.js";
|
||||
import { HDL, HdlParse, Part, PinParts } from "../languages/hdl.js";
|
||||
import { getBuiltinChip, hasBuiltinChip } from "./builtins/index.js";
|
||||
import { Chip, Connection, isConstantPin } from "./chip.js";
|
||||
|
||||
function pinWidth(pin: PinParts): Result<number | undefined, CompilationError> {
|
||||
const start = pin.start ?? 0;
|
||||
if (pin.end === undefined) {
|
||||
return Ok(undefined);
|
||||
}
|
||||
if (pin.end >= start) {
|
||||
return Ok(pin.end - start + 1);
|
||||
}
|
||||
return Err(
|
||||
createError(
|
||||
`Bus start index should be less than or equal to bus end index`,
|
||||
pin.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function parse(
|
||||
code: string,
|
||||
dir?: string,
|
||||
name?: string,
|
||||
fs?: FileSystem,
|
||||
): Promise<Result<Chip, CompilationError>> {
|
||||
const parsed = HDL.parse(code.toString());
|
||||
if (isErr(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return build({ parts: Ok(parsed), dir, name, fs });
|
||||
}
|
||||
|
||||
export async function loadChip(
|
||||
name: string,
|
||||
dir?: string,
|
||||
fs?: FileSystem,
|
||||
): Promise<Result<Chip>> {
|
||||
if (hasBuiltinChip(name) || fs === undefined) {
|
||||
return await getBuiltinChip(name);
|
||||
}
|
||||
try {
|
||||
const file = await fs.readFile(`${dir}/${name}.hdl`);
|
||||
const maybeParsedHDL = HDL.parse(file);
|
||||
|
||||
let maybeChip: Result<Chip, Error>;
|
||||
if (isOk(maybeParsedHDL)) {
|
||||
const maybeBuilt = await build({
|
||||
parts: Ok(maybeParsedHDL),
|
||||
dir,
|
||||
name,
|
||||
fs,
|
||||
});
|
||||
if (isErr(maybeBuilt)) {
|
||||
maybeChip = Err(new Error(Err(maybeBuilt).message));
|
||||
} else {
|
||||
maybeChip = maybeBuilt;
|
||||
}
|
||||
} else {
|
||||
maybeChip = Err(new Error("HDL Was not parsed"));
|
||||
}
|
||||
|
||||
return maybeChip;
|
||||
} catch (_e) {
|
||||
return Err(new Error(`Could not load chip ${name}.hdl` /*, { cause: e }*/));
|
||||
}
|
||||
}
|
||||
|
||||
export async function build(
|
||||
...args: Parameters<typeof ChipBuilder.build>
|
||||
): Promise<ReturnType<typeof ChipBuilder.build>> {
|
||||
return await ChipBuilder.build(...args);
|
||||
}
|
||||
|
||||
interface InternalPin {
|
||||
isDefined: boolean;
|
||||
firstUse: Span;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface WireData {
|
||||
partChip: Chip;
|
||||
lhs: PinParts;
|
||||
rhs: PinParts;
|
||||
}
|
||||
|
||||
function getSubBusWidth(pin: PinParts): number | undefined {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
return pin.end - pin.start + 1;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function display(pin: PinParts): string {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
return `${pin.pin}[${pin.start}..${pin.end}]`;
|
||||
}
|
||||
return pin.pin;
|
||||
}
|
||||
|
||||
function createConnection(
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<Connection, CompilationError> {
|
||||
const lhsWidth = pinWidth(lhs);
|
||||
const rhsWidth = pinWidth(rhs);
|
||||
if (isErr(lhsWidth)) {
|
||||
return lhsWidth;
|
||||
}
|
||||
if (isErr(rhsWidth)) {
|
||||
return rhsWidth;
|
||||
}
|
||||
|
||||
return Ok({
|
||||
to: {
|
||||
name: lhs.pin.toString(),
|
||||
start: lhs.start ?? 0,
|
||||
width: Ok(lhsWidth),
|
||||
},
|
||||
from: {
|
||||
name: rhs.pin.toString(),
|
||||
start: rhs.start ?? 0,
|
||||
width: Ok(rhsWidth),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getIndices(pin: PinParts): number[] {
|
||||
if (pin.start != undefined && pin.end != undefined) {
|
||||
const indices = [];
|
||||
for (let i = pin.start; i <= pin.end; i++) {
|
||||
indices.push(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
return [-1];
|
||||
}
|
||||
|
||||
function checkMultipleAssignments(
|
||||
pin: PinParts,
|
||||
assignedIndexes: Map<string, Set<number>>,
|
||||
): Result<void, CompilationError> {
|
||||
let errorIndex: number | undefined = undefined; // -1 stands for the whole bus width
|
||||
const indices = assignedIndexes.get(pin.pin);
|
||||
if (!indices) {
|
||||
assignedIndexes.set(pin.pin, new Set(getIndices(pin)));
|
||||
} else {
|
||||
if (indices.has(-1)) {
|
||||
errorIndex = pin.start ?? -1;
|
||||
} else if (pin.start !== undefined && pin.end !== undefined) {
|
||||
for (const i of getIndices(pin)) {
|
||||
if (indices.has(i)) {
|
||||
errorIndex = i;
|
||||
}
|
||||
indices.add(i);
|
||||
}
|
||||
} else {
|
||||
indices.add(-1);
|
||||
}
|
||||
}
|
||||
if (errorIndex != undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
`Cannot write to pin ${pin.pin}${
|
||||
errorIndex != -1 ? `[${errorIndex}]` : ""
|
||||
} multiple times`,
|
||||
pin.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
class ChipBuilder {
|
||||
private parts: HdlParse;
|
||||
private fs?: FileSystem;
|
||||
private dir?: string;
|
||||
private expectedName?: string;
|
||||
|
||||
private chip: Chip;
|
||||
private internalPins: Map<string, InternalPin> = new Map();
|
||||
private inPins: Map<string, Set<number>> = new Map();
|
||||
private outPins: Map<string, Set<number>> = new Map();
|
||||
private wires: WireData[] = [];
|
||||
|
||||
static build(options: {
|
||||
parts: HdlParse;
|
||||
fs?: FileSystem;
|
||||
dir?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
return new ChipBuilder(options).build();
|
||||
}
|
||||
|
||||
private constructor({
|
||||
parts,
|
||||
fs,
|
||||
dir,
|
||||
name,
|
||||
}: {
|
||||
parts: HdlParse;
|
||||
fs?: FileSystem;
|
||||
dir?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
this.parts = parts;
|
||||
this.expectedName = name;
|
||||
this.dir = dir;
|
||||
this.fs = fs;
|
||||
this.chip = new Chip(
|
||||
parts.ins.map(({ pin, width }) => ({ pin: pin.toString(), width })),
|
||||
parts.outs.map(({ pin, width }) => ({ pin: pin.toString(), width })),
|
||||
parts.name.value,
|
||||
[],
|
||||
parts.clocked,
|
||||
);
|
||||
}
|
||||
|
||||
async build(): Promise<Result<Chip, CompilationError>> {
|
||||
if (this.expectedName && this.parts.name.value != this.expectedName) {
|
||||
return Err(createError(`Wrong chip name`, this.parts.name.span));
|
||||
}
|
||||
|
||||
if (this.parts.parts === "BUILTIN") {
|
||||
return await getBuiltinChip(this.parts.name.value);
|
||||
}
|
||||
|
||||
const result = await this.wireParts();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.chip.clockedPins = new Set(
|
||||
[...this.chip.ins.entries(), ...this.chip.outs.entries()]
|
||||
.map((pin) => pin.name)
|
||||
.filter((pin) => this.chip.isClockedPin(pin)),
|
||||
);
|
||||
|
||||
// Reset clock order after wiring sub-pins
|
||||
for (const part of this.chip.parts) {
|
||||
part.subscribeToClock();
|
||||
}
|
||||
|
||||
return Ok(this.chip);
|
||||
}
|
||||
|
||||
private async wireParts(): Promise<Result<void, CompilationError>> {
|
||||
if (this.parts.parts === "BUILTIN") {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
for (const part of this.parts.parts) {
|
||||
const builtin = await loadChip(part.name, this.dir, this.fs);
|
||||
if (isErr(builtin)) {
|
||||
return Err(createError(`Undefined chip name: ${part.name}`, part.span));
|
||||
}
|
||||
const partChip = Ok(builtin);
|
||||
if (partChip.name == this.chip.name) {
|
||||
return Err(
|
||||
createError(
|
||||
`Cannot use chip ${partChip.name} to implement itself`,
|
||||
part.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
const result = this.wirePart(part, partChip);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
let result = this.validateInternalPins();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
// We need to check this at the end because during wiring we might not know the width of some internal pins
|
||||
result = this.validateWireWidths();
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private checkLoops(
|
||||
part: Part,
|
||||
partChip: Chip,
|
||||
): Result<void, CompilationError> {
|
||||
const ins = new Set<string>();
|
||||
const outs = new Set<string>();
|
||||
|
||||
let loop: string | undefined = undefined;
|
||||
for (const { lhs, rhs } of part.wires) {
|
||||
if (partChip.isInPin(lhs.pin)) {
|
||||
if (outs.has(rhs.pin)) {
|
||||
loop = rhs.pin;
|
||||
break;
|
||||
} else {
|
||||
ins.add(rhs.pin);
|
||||
}
|
||||
} else if (partChip.isOutPin(lhs.pin)) {
|
||||
if (ins.has(rhs.pin)) {
|
||||
loop = rhs.pin;
|
||||
break;
|
||||
} else {
|
||||
outs.add(rhs.pin);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loop) {
|
||||
return Err(createError(`Looping wire ${loop}`, part.span));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private wirePart(part: Part, partChip: Chip): Result<void, CompilationError> {
|
||||
const result = this.checkLoops(part, partChip);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const connections: Connection[] = [];
|
||||
this.inPins.clear();
|
||||
for (const { lhs, rhs } of part.wires) {
|
||||
const result = this.validateWire(partChip, lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
const connection = createConnection(lhs, rhs);
|
||||
if (isErr(connection)) {
|
||||
return connection;
|
||||
}
|
||||
connections.push(Ok(connection));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.chip.wire(partChip, connections);
|
||||
if (isErr(result)) {
|
||||
const error = Err(result);
|
||||
return Err(
|
||||
createError(
|
||||
error.message,
|
||||
error.lhs
|
||||
? part.wires[error.wireIndex].lhs.span
|
||||
: part.wires[error.wireIndex].rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.chip.sortParts();
|
||||
return Ok();
|
||||
} catch (e) {
|
||||
return Err(createError((e as Error).message, part.span));
|
||||
}
|
||||
}
|
||||
|
||||
private validateWire(
|
||||
partChip: Chip,
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
if (partChip.isInPin(lhs.pin)) {
|
||||
const result = this.validateInputWire(lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else if (partChip.isOutPin(lhs.pin)) {
|
||||
const result = this.validateOutputWire(partChip, lhs, rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return Err(createError(`Undefined pin name: ${lhs.pin}`, lhs.span));
|
||||
}
|
||||
if (!isConstantPin(rhs.pin)) {
|
||||
this.wires.push({ partChip: partChip, lhs, rhs });
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInputWire(
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
let result = this.validateInputSource(rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
result = checkMultipleAssignments(lhs, this.inPins);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// track internal pin use to detect undefined pins
|
||||
if (this.chip.isInternalPin(rhs.pin)) {
|
||||
const pinData = this.internalPins.get(rhs.pin);
|
||||
if (pinData == undefined) {
|
||||
this.internalPins.set(rhs.pin, {
|
||||
isDefined: false,
|
||||
firstUse: rhs.span,
|
||||
});
|
||||
} else {
|
||||
pinData.firstUse =
|
||||
pinData.firstUse.start < rhs.span.start ? pinData.firstUse : rhs.span;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateOutputWire(
|
||||
partChip: Chip,
|
||||
lhs: PinParts,
|
||||
rhs: PinParts,
|
||||
): Result<void, CompilationError> {
|
||||
let result = this.validateWriteTarget(rhs);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this.chip.isOutPin(rhs.pin)) {
|
||||
result = checkMultipleAssignments(rhs, this.outPins);
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// rhs is necessarily an internal pin
|
||||
if (rhs.start !== undefined || rhs.end !== undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
`Internal pins (in this case: ${rhs.pin}) cannot be subscripted or indexed`,
|
||||
rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
// track internal pin creation to detect undefined pins
|
||||
const pinData = this.internalPins.get(rhs.pin);
|
||||
const width = getSubBusWidth(lhs) ?? partChip.get(lhs.pin)?.width;
|
||||
if (pinData == undefined) {
|
||||
this.internalPins.set(rhs.pin, {
|
||||
isDefined: true,
|
||||
firstUse: rhs.span,
|
||||
width,
|
||||
});
|
||||
} else {
|
||||
if (pinData.isDefined) {
|
||||
return Err(
|
||||
createError(`Internal pin ${rhs.pin} already defined`, rhs.span),
|
||||
);
|
||||
}
|
||||
pinData.isDefined = true;
|
||||
pinData.width = width;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateWriteTarget(rhs: PinParts): Result<void, CompilationError> {
|
||||
if (this.chip.isInPin(rhs.pin)) {
|
||||
return Err(createError(`Cannot write to input pin ${rhs.pin}`, rhs.span));
|
||||
}
|
||||
if (isConstantPin(rhs.pin)) {
|
||||
return Err(
|
||||
createError(`Internal pin name cannot be "true" or "false"`, rhs.span),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInputSource(rhs: PinParts): Result<void, CompilationError> {
|
||||
if (this.chip.isOutPin(rhs.pin)) {
|
||||
return Err(createError(`Cannot use output pin as input`, rhs.span));
|
||||
} else if (!this.chip.isInPin(rhs.pin) && rhs.start != undefined) {
|
||||
return Err(
|
||||
createError(
|
||||
isConstantPin(rhs.pin)
|
||||
? `Constant bus cannot be subscripted or indexed`
|
||||
: `Internal pins (in this case: ${rhs.pin}) cannot be subscripted or indexed`,
|
||||
rhs.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateInternalPins(): Result<void, CompilationError> {
|
||||
for (const [name, pinData] of this.internalPins) {
|
||||
if (!pinData.isDefined) {
|
||||
return Err(
|
||||
createError(
|
||||
name.toLowerCase() == "true" || name.toLowerCase() == "false"
|
||||
? `The constant bus ${name.toLowerCase()} must be in lower-case`
|
||||
: `Undefined internal pin name: ${name}`,
|
||||
pinData.firstUse,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private validateWireWidths(): Result<void, CompilationError> {
|
||||
for (const wire of this.wires) {
|
||||
const lhsWidth =
|
||||
getSubBusWidth(wire.lhs) ?? wire.partChip.get(wire.lhs.pin)?.width;
|
||||
const rhsWidth =
|
||||
getSubBusWidth(wire.rhs) ??
|
||||
this.chip.get(wire.rhs.pin)?.width ??
|
||||
this.internalPins.get(wire.rhs.pin)?.width;
|
||||
if (lhsWidth != rhsWidth) {
|
||||
return Err(
|
||||
createError(
|
||||
`Different bus widths: ${display(
|
||||
wire.lhs,
|
||||
)}(${lhsWidth}) and ${display(wire.rhs)}(${rhsWidth})`,
|
||||
{
|
||||
start: wire.lhs.span.start,
|
||||
end: wire.rhs.span.end,
|
||||
line: wire.lhs.span.line,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Memory } from "./builtins/computer/computer.js";
|
||||
import { RAM } from "./builtins/sequential/ram.js";
|
||||
import { Chip, Pin } from "./chip.js";
|
||||
|
||||
export function getBuiltinValue(
|
||||
chip: string,
|
||||
part: Chip,
|
||||
idx: number,
|
||||
): Pin | undefined {
|
||||
switch (chip) {
|
||||
case "Register":
|
||||
case "ARegister":
|
||||
case "DRegister":
|
||||
case "PC":
|
||||
case "KEYBOARD":
|
||||
return part.out();
|
||||
case "RAM8":
|
||||
case "RAM64":
|
||||
case "RAM512":
|
||||
case "RAM4K":
|
||||
case "RAM16K":
|
||||
case "ROM32K":
|
||||
case "Screen":
|
||||
return (part as RAM).at(idx);
|
||||
case "Memory":
|
||||
return (part as Memory).at(idx);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Ok, unwrap } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { CHIP_PROJECTS } from "@nand2tetris/projects/base.js";
|
||||
import { ChipProjects } from "@nand2tetris/projects/full.js";
|
||||
import { Max } from "@nand2tetris/projects/samples/hack.js";
|
||||
import { compare } from "../../compare.js";
|
||||
import { CMP, Cmp } from "../../languages/cmp.js";
|
||||
import { HDL, HdlParse } from "../../languages/hdl.js";
|
||||
import { TST, Tst } from "../../languages/tst.js";
|
||||
import { ChipTest } from "../../test/chiptst.js";
|
||||
import { build } from "../builder.js";
|
||||
import { Chip } from "../chip.js";
|
||||
|
||||
const SKIP = new Set<string>(["Computer", "Memory"]);
|
||||
|
||||
describe("All Projects", () => {
|
||||
describe.each(Object.keys(CHIP_PROJECTS))("project %s", (project) => {
|
||||
it.each(
|
||||
CHIP_PROJECTS[project as keyof typeof CHIP_PROJECTS].filter(
|
||||
(k) => !SKIP.has(k),
|
||||
),
|
||||
)("Builtin %s", async (chipName) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const ChipProject = ChipProjects[project].CHIPS;
|
||||
let hdlFile: string = ChipProject[`${chipName}.hdl`];
|
||||
const tstFile: string = ChipProject[`${chipName}.tst`];
|
||||
const cmpFile: string = ChipProject[`${chipName}.cmp`];
|
||||
|
||||
expect(hdlFile).toBeDefined();
|
||||
expect(tstFile).toBeDefined();
|
||||
expect(cmpFile).toBeDefined();
|
||||
|
||||
const partsIdx = hdlFile.indexOf("PARTS:");
|
||||
expect(partsIdx).toBeGreaterThan(0);
|
||||
hdlFile = hdlFile.substring(0, partsIdx) + "BUILTIN; }";
|
||||
const hdl = HDL.parse(hdlFile);
|
||||
expect(hdl).toBeOk();
|
||||
const tst = TST.parse(tstFile);
|
||||
expect(tst).toBeOk();
|
||||
|
||||
const chip = await build({ parts: Ok(hdl as Ok<HdlParse>) });
|
||||
expect(chip).toBeOk();
|
||||
const test = unwrap(ChipTest.from(Ok(tst as Ok<Tst>))).with(
|
||||
Ok(chip as Ok<Chip>),
|
||||
);
|
||||
|
||||
if (project === "05") {
|
||||
test.setFileSystem(
|
||||
new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "/samples/Max.hack": Max }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await test.run();
|
||||
|
||||
const outFile = test.log();
|
||||
|
||||
const cmp = CMP.parse(cmpFile);
|
||||
expect(cmp).toBeOk();
|
||||
const out = CMP.parse(outFile);
|
||||
expect(out).toBeOk();
|
||||
|
||||
const diffs = compare(Ok(cmp as Ok<Cmp>), Ok(out as Ok<Cmp>));
|
||||
expect(diffs).toHaveNoDiff();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Chip } from "../../chip.js";
|
||||
|
||||
export function add16(a: number, b: number): [number] {
|
||||
return [(a + b) & 0xffff];
|
||||
}
|
||||
|
||||
export class Add16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"], "Add16");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = add16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { alu, alua, COMMANDS_OP, Flags } from "../../../cpu/alu.js";
|
||||
import { Chip, HIGH, LOW } from "../../chip.js";
|
||||
|
||||
export class ALUNoStat extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out] = alu(op, x, y);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class ALU extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
"zr", // 1 if (out === 0), 0 otherwise
|
||||
"ng", // 1 if (out < 0), 0 otherwise
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out, flags] = alu(op, x, y);
|
||||
|
||||
const ng = flags === Flags.Negative ? HIGH : LOW;
|
||||
const zr = flags === Flags.Zero ? HIGH : LOW;
|
||||
this.out("out").busVoltage = out;
|
||||
this.out("ng").pull(ng);
|
||||
this.out("zr").pull(zr);
|
||||
}
|
||||
|
||||
op(): COMMANDS_OP {
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
return op as COMMANDS_OP;
|
||||
}
|
||||
}
|
||||
|
||||
export class ALUAll extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"x[16]",
|
||||
"y[16]", // 16-bit inputs
|
||||
"zx", // zero the x input?
|
||||
"nx", // negate the x input?
|
||||
"zy", // zero the y input?
|
||||
"ny", // negate the y input?
|
||||
"f", // compute out = x + y (if 1) or x & y (if 0)
|
||||
"no", // negate the out output?
|
||||
],
|
||||
[
|
||||
"out[16]", // 16-bit output
|
||||
"zr", // 1 if (out === 0), 0 otherwise
|
||||
"ng", // 1 if (out < 0), 0 otherwise
|
||||
],
|
||||
"ALU",
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const x = this.in("x").busVoltage;
|
||||
const y = this.in("y").busVoltage;
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
|
||||
const [out, flags] = alua(op, x, y);
|
||||
|
||||
const ng = flags === Flags.Negative ? HIGH : LOW;
|
||||
const zr = flags === Flags.Zero ? HIGH : LOW;
|
||||
this.out("out").busVoltage = out;
|
||||
this.out("ng").pull(ng);
|
||||
this.out("zr").pull(zr);
|
||||
}
|
||||
|
||||
op(): COMMANDS_OP {
|
||||
const zx = this.in("zx").busVoltage << 5;
|
||||
const nx = this.in("nx").busVoltage << 4;
|
||||
const zy = this.in("zy").busVoltage << 3;
|
||||
const ny = this.in("ny").busVoltage << 2;
|
||||
const f = this.in("f").busVoltage << 1;
|
||||
const no = this.in("no").busVoltage << 0;
|
||||
const op = zx + nx + zy + ny + f + no;
|
||||
return op as COMMANDS_OP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Chip, Voltage } from "../../chip.js";
|
||||
import { or } from "../logic/or.js";
|
||||
import { halfAdder } from "./half_adder.js";
|
||||
|
||||
export function fullAdder(
|
||||
a: Voltage,
|
||||
b: Voltage,
|
||||
c: Voltage,
|
||||
): [Voltage, Voltage] {
|
||||
const [s, ca] = halfAdder(a, b);
|
||||
const [sum, cb] = halfAdder(s, c);
|
||||
const [carry] = or(ca, cb);
|
||||
|
||||
return [sum, carry];
|
||||
}
|
||||
|
||||
export class FullAdder extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b", "c"], ["sum", "carry"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const c = this.in("c").voltage();
|
||||
const [sum, carry] = fullAdder(a, b, c);
|
||||
this.out("sum").pull(sum);
|
||||
this.out("carry").pull(carry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function halfAdder(a: Voltage, b: Voltage): [Voltage, Voltage] {
|
||||
const sum = (a === 1 && b === 0) || (a === 0 && b === 1) ? HIGH : LOW;
|
||||
const car = a === 1 && b === 1 ? HIGH : LOW;
|
||||
|
||||
return [sum, car];
|
||||
}
|
||||
|
||||
export class HalfAdder extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["sum", "carry"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [sum, carry] = halfAdder(a, b);
|
||||
this.out("sum").pull(sum);
|
||||
this.out("carry").pull(carry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Chip } from "../../chip.js";
|
||||
import { add16 } from "./add_16.js";
|
||||
|
||||
export function inc16(n: number): [number] {
|
||||
return add16(n, 1);
|
||||
}
|
||||
|
||||
export class Inc16 extends Chip {
|
||||
constructor() {
|
||||
super(["in[16]"], ["out[16]"], "Inc16");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in().busVoltage;
|
||||
const [out] = inc16(a);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const builtinOverrides: Record<string, string> = {
|
||||
CPU: `CHIP CPU {
|
||||
|
||||
IN inM[16], // M value input (M = contents of RAM[A])
|
||||
instruction[16], // Instruction for execution
|
||||
reset; // Signals whether to re-start the current
|
||||
// program (reset==1) or continue executing
|
||||
// the current program (reset==0).
|
||||
|
||||
OUT outM[16], // M value output
|
||||
writeM, // Write to M?
|
||||
addressM[15], // Address in data memory (of M)
|
||||
pc[15]; // address of next instruction
|
||||
|
||||
PARTS:
|
||||
Mux16(a=instruction, b=ALUoutput, sel=instruction[15], out=Ainput);
|
||||
Not(in=instruction[15], out=Ainstruction);
|
||||
Or(a=Ainstruction, b=instruction[5], out=loadA);
|
||||
ARegister(in=Ainput, load=loadA, out=Aoutput, out[0..14]=addressM);
|
||||
|
||||
And(a=instruction[15], b=instruction[4], out=loadD);
|
||||
DRegister(in=ALUoutput, load=loadD, out=Doutput);
|
||||
|
||||
Mux16(a=Aoutput, b=inM, sel=instruction[12], out=ALUsecondInput);
|
||||
|
||||
ALU(x=Doutput, y=ALUsecondInput,
|
||||
zx=instruction[11], nx=instruction[10],
|
||||
zy=instruction[9], ny=instruction[8],
|
||||
f=instruction[7], no=instruction[6],
|
||||
out=ALUoutput, out=outM, ng=negative, zr=zero);
|
||||
|
||||
And(a=instruction[15], b=instruction[3], out=writeM);
|
||||
|
||||
Or(a=negative, b=zero, out=notPositive);
|
||||
Not(in=notPositive, out=positive);
|
||||
|
||||
And(a=positive, b=instruction[0], out=j1);
|
||||
And(a=zero, b=instruction[1], out=j2);
|
||||
And(a=negative, b=instruction[2], out=j3);
|
||||
Or(a=j1, b=j2, out=jTemp);
|
||||
Or(a=jTemp, b=j3, out=jumpIfC);
|
||||
And(a=jumpIfC, b=instruction[15], out=jump);
|
||||
|
||||
PC(reset=reset, inc=true, load=jump, in=Aoutput, out[0..14]=pc);
|
||||
}`,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
# Computer Builtins
|
||||
|
||||
## CPU
|
||||
|
||||
<iframe style="border: 1px solid rgba(0, 0, 0, 0.1);" width="800" height="450" src="https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FNR57Ym7f7zUowcs0jrtTsO%2FHack-CPU%3Fnode-id%3D0%253A1" allowfullscreen></iframe>
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Max } from "@nand2tetris/projects/samples/hack.js";
|
||||
import { HIGH } from "../../chip.js";
|
||||
import { CPU, Memory, ROM32K } from "./computer.js";
|
||||
|
||||
describe("Computer Chip Builtins", () => {
|
||||
describe("ROM Builtin", () => {
|
||||
it("can load a file", async () => {
|
||||
const fs = new FileSystem(
|
||||
new ObjectFileSystemAdapter({ "Max.hack": Max }),
|
||||
);
|
||||
const rom = new ROM32K();
|
||||
|
||||
await rom.load(fs, "Max.hack");
|
||||
|
||||
expect(rom.at(4).busVoltage).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CPU Chip Builtin", () => {
|
||||
it("updates PC on tock", () => {
|
||||
const cpu = new CPU();
|
||||
|
||||
cpu.in("instruction").busVoltage = 12345;
|
||||
|
||||
cpu.tick();
|
||||
|
||||
expect(cpu.out("pc").busVoltage).toBe(0);
|
||||
|
||||
cpu.tock();
|
||||
|
||||
expect(cpu.out("pc").busVoltage).toBe(1);
|
||||
});
|
||||
|
||||
it("updtates writeM on tick", () => {
|
||||
const cpu = new CPU();
|
||||
|
||||
cpu.in("instruction").busVoltage = 0b1110_1111_1100_1000;
|
||||
|
||||
cpu.tick();
|
||||
|
||||
expect(cpu.out("writeM").voltage()).toBe(HIGH);
|
||||
expect(cpu.out("outM").busVoltage).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory", () => {
|
||||
it("maps addresses greater than KBD as 0", () => {
|
||||
const memory = new Memory();
|
||||
memory.in("address").busVoltage = 24577;
|
||||
memory.in("in").busVoltage = 47;
|
||||
|
||||
memory.eval();
|
||||
|
||||
expect(memory.out().busVoltage).toBe(0);
|
||||
|
||||
memory.in("load").busVoltage = HIGH;
|
||||
|
||||
memory.eval();
|
||||
|
||||
expect(memory.out().busVoltage).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
CPUInput,
|
||||
CPUState,
|
||||
cpuTick,
|
||||
cpuTock,
|
||||
emptyState,
|
||||
} from "../../../cpu/cpu.js";
|
||||
import {
|
||||
KEYBOARD_OFFSET,
|
||||
KeyboardAdapter,
|
||||
SCREEN_OFFSET,
|
||||
SCREEN_SIZE,
|
||||
} from "../../../cpu/memory.js";
|
||||
import { load } from "../../../fs.js";
|
||||
import { int10 } from "../../../util/twos.js";
|
||||
import {
|
||||
Bus,
|
||||
Chip,
|
||||
ClockedChip,
|
||||
ConstantBus,
|
||||
FALSE_BUS,
|
||||
HIGH,
|
||||
LOW,
|
||||
Pin,
|
||||
} from "../../chip.js";
|
||||
import { RAM, RAM16K } from "../sequential/ram.js";
|
||||
|
||||
export class ROM32K extends RAM {
|
||||
constructor() {
|
||||
super(15, "ROM");
|
||||
}
|
||||
|
||||
override async load(fs: FileSystem, path: string) {
|
||||
try {
|
||||
(await load(fs, path)).map((v, i) => (this.at(i).busVoltage = v));
|
||||
} catch (cause) {
|
||||
// throw new Error(`ROM32K Failed to load file ${path}`, { cause });
|
||||
throw new Error(`ROM32K Failed to load file ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Screen extends RAM {
|
||||
static readonly SIZE = SCREEN_SIZE;
|
||||
static readonly OFFSET = SCREEN_OFFSET;
|
||||
|
||||
constructor() {
|
||||
super(13, "Screen");
|
||||
}
|
||||
}
|
||||
|
||||
export class Keyboard extends Chip implements KeyboardAdapter {
|
||||
static readonly OFFSET = KEYBOARD_OFFSET;
|
||||
|
||||
constructor() {
|
||||
super([], ["out[16]"], "Keyboard");
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.out().busVoltage;
|
||||
}
|
||||
|
||||
setKey(key: number) {
|
||||
this.out().busVoltage = key & 0xffff;
|
||||
}
|
||||
|
||||
clearKey() {
|
||||
this.out().busVoltage = 0;
|
||||
}
|
||||
|
||||
override get(name: string) {
|
||||
return name === this.name
|
||||
? new ConstantBus(this.name, this.getKey()) // readonly
|
||||
: super.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class Memory extends ClockedChip {
|
||||
readonly ram = new RAM16K();
|
||||
readonly screen = new Screen();
|
||||
private keyboard = new Keyboard();
|
||||
private address = 0;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
["in[16]", "load", "address[15])"],
|
||||
["out[16]"],
|
||||
"Memory",
|
||||
[],
|
||||
["in", "load"]
|
||||
);
|
||||
this.parts.push(this.keyboard);
|
||||
this.parts.push(this.screen);
|
||||
this.parts.push(this.ram);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
const load = this.in("load").voltage();
|
||||
this.address = this.in("address").busVoltage;
|
||||
if (load) {
|
||||
const inn = this.in().busVoltage;
|
||||
if (this.address > Keyboard.OFFSET) {
|
||||
// out of "physical" bounds, should result in some kind of issue...
|
||||
}
|
||||
if (this.address == Keyboard.OFFSET) {
|
||||
// Keyboard, do nothing
|
||||
} else if (this.address >= Screen.OFFSET) {
|
||||
this.screen.at(this.address - Screen.OFFSET).busVoltage = inn;
|
||||
} else {
|
||||
this.ram.at(this.address).busVoltage = inn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
override eval() {
|
||||
if (!this.ram) return;
|
||||
this.address = this.in("address").busVoltage;
|
||||
let out = 0;
|
||||
if (this.address > Keyboard.OFFSET) {
|
||||
// out of "physical" bounds, should result in some kind of issue...
|
||||
} else if (this.address == Keyboard.OFFSET) {
|
||||
out = this.keyboard?.out().busVoltage ?? 0;
|
||||
} else if (this.address >= Screen.OFFSET) {
|
||||
out = this.screen?.at(this.address - Screen.OFFSET).busVoltage ?? 0;
|
||||
} else {
|
||||
out = this.ram?.at(this.address).busVoltage ?? 0;
|
||||
}
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
|
||||
override in(pin?: string): Pin {
|
||||
if (pin?.startsWith("RAM16K")) {
|
||||
const idx = int10(pin.match(/\[(?<idx>\d+)]/)?.groups?.idx ?? "0");
|
||||
return this.ram.at(idx);
|
||||
}
|
||||
if (pin?.startsWith("Screen")) {
|
||||
const idx = int10(pin.match(/\[(?<idx>\d+)]/)?.groups?.idx ?? "0");
|
||||
return this.screen.at(idx);
|
||||
}
|
||||
if (pin?.startsWith("Keyboard")) {
|
||||
return this.keyboard.out();
|
||||
}
|
||||
return super.in(pin);
|
||||
}
|
||||
|
||||
override get(name: string, offset = 0): Pin | undefined {
|
||||
if (name.startsWith("RAM16K")) {
|
||||
return this.at(offset & 0x3fff);
|
||||
}
|
||||
if (name.startsWith("Screen")) {
|
||||
return this.at(offset & (0x1fff + Screen.OFFSET));
|
||||
}
|
||||
if (name.startsWith("Keyboard")) {
|
||||
return this.at(Keyboard.OFFSET);
|
||||
}
|
||||
if (name.startsWith("Memory")) {
|
||||
return this.at(offset);
|
||||
}
|
||||
return super.get(name, offset);
|
||||
}
|
||||
|
||||
at(offset: number): Pin {
|
||||
if (offset > Keyboard.OFFSET) {
|
||||
return FALSE_BUS;
|
||||
}
|
||||
if (offset == Keyboard.OFFSET) {
|
||||
return this.keyboard.out();
|
||||
}
|
||||
if (offset >= Screen.OFFSET) {
|
||||
return this.screen.at(offset - Screen.OFFSET);
|
||||
}
|
||||
return this.ram.at(offset);
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this.address = 0;
|
||||
this.ram.reset();
|
||||
this.screen.reset();
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
class DRegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.D;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.D = num;
|
||||
}
|
||||
}
|
||||
|
||||
class ARegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.A;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.A = num;
|
||||
}
|
||||
}
|
||||
|
||||
class PCBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private cpu: CPUState,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.cpu.PC;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.cpu.PC = num;
|
||||
}
|
||||
}
|
||||
|
||||
export class CPU extends ClockedChip {
|
||||
private _state: CPUState = emptyState();
|
||||
|
||||
get state(): CPUState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
["inM[16]", "instruction[16]", "reset"],
|
||||
["outM[16]", "writeM", "addressM[15]", "pc[15]"],
|
||||
"CPU",
|
||||
[],
|
||||
["pc", "addressM", "reset"],
|
||||
);
|
||||
}
|
||||
|
||||
override tick(): void {
|
||||
const [state, writeM] = cpuTick(this.cpuInput(), this._state);
|
||||
this._state = state;
|
||||
this.out("writeM").pull(writeM ? HIGH : LOW);
|
||||
this.out("outM").busVoltage = this._state.ALU ?? 0;
|
||||
}
|
||||
|
||||
override tock(): void {
|
||||
if (!this._state) return; // Skip initial tock
|
||||
const [output, state] = cpuTock(this.cpuInput(), this._state);
|
||||
this._state = state;
|
||||
|
||||
this.out("addressM").busVoltage = output.addressM ?? 0;
|
||||
this.out("outM").busVoltage = output.outM ?? 0;
|
||||
this.out("writeM").pull(output.writeM ? HIGH : LOW);
|
||||
this.out("pc").busVoltage = this._state?.PC ?? 0;
|
||||
}
|
||||
|
||||
private cpuInput(): CPUInput {
|
||||
const inM = this.in("inM").busVoltage;
|
||||
const instruction = this.in("instruction").busVoltage;
|
||||
const reset = this.in("reset").busVoltage === 1;
|
||||
return { inM, instruction, reset };
|
||||
}
|
||||
|
||||
override get(pin: string, offset?: number): Pin | undefined {
|
||||
if (pin?.startsWith("ARegister")) {
|
||||
return new ARegisterBus("ARegister", this._state);
|
||||
}
|
||||
if (pin?.startsWith("DRegister")) {
|
||||
return new DRegisterBus("DRegister", this._state);
|
||||
}
|
||||
if (pin?.startsWith("PC")) {
|
||||
return new PCBus("PC", this._state);
|
||||
}
|
||||
return super.get(pin, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this._state = emptyState();
|
||||
|
||||
// This is a bit of a hack, but because super.reset() does ticktock,
|
||||
// we need to set PC to -1, so that it will be 0 after the reset
|
||||
this._state.PC = -1;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class Computer extends Chip {
|
||||
readonly cpu = new CPU();
|
||||
readonly ram = new Memory();
|
||||
readonly rom = new ROM32K();
|
||||
|
||||
constructor() {
|
||||
super(["reset"], []);
|
||||
|
||||
this.wire(this.cpu, [
|
||||
{ from: { name: "reset", start: 0 }, to: { name: "reset", start: 0 } },
|
||||
{
|
||||
from: { name: "instruction", start: 0 },
|
||||
to: { name: "instruction", start: 0 },
|
||||
},
|
||||
{ from: { name: "oldOutM", start: 0 }, to: { name: "inM", start: 0 } },
|
||||
{ from: { name: "writeM", start: 0 }, to: { name: "writeM", start: 0 } },
|
||||
{
|
||||
from: { name: "addressM", start: 0 },
|
||||
to: { name: "addressM", start: 0 },
|
||||
},
|
||||
{ from: { name: "newInM", start: 0 }, to: { name: "outM", start: 0 } },
|
||||
{ from: { name: "pc", start: 0 }, to: { name: "pc", start: 0 } },
|
||||
]);
|
||||
|
||||
this.wire(this.rom, [
|
||||
{ from: { name: "pc", start: 0 }, to: { name: "address", start: 0 } },
|
||||
{
|
||||
from: { name: "instruction", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
this.wire(this.ram, [
|
||||
{ from: { name: "newInM", start: 0 }, to: { name: "in", start: 0 } },
|
||||
{ from: { name: "writeM", start: 0 }, to: { name: "load", start: 0 } },
|
||||
{
|
||||
from: { name: "addressM", start: 0 },
|
||||
to: { name: "address", start: 0 },
|
||||
},
|
||||
{ from: { name: "oldOutM", start: 0 }, to: { name: "out", start: 0 } },
|
||||
]);
|
||||
|
||||
for (const pin of [...this.ins.entries(), ...this.outs.entries()]) {
|
||||
if (this.isClockedPin(pin.name)) {
|
||||
this.clockedPins.add(pin.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override eval() {
|
||||
super.eval();
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
if (
|
||||
name.startsWith("PC") ||
|
||||
name.startsWith("ARegister") ||
|
||||
name.startsWith("DRegister")
|
||||
) {
|
||||
return this.cpu.get(name);
|
||||
}
|
||||
if (name.startsWith("RAM16K")) {
|
||||
return this.ram.get(name, offset);
|
||||
}
|
||||
return super.get(name, offset);
|
||||
}
|
||||
|
||||
override async load(fs: FileSystem, path: string): Promise<void> {
|
||||
return await this.rom.load(fs, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { parse } from "../builder.js";
|
||||
import { Chip } from "../chip.js";
|
||||
import { Add16 } from "./arithmetic/add_16.js";
|
||||
import { ALU, ALUNoStat } from "./arithmetic/alu.js";
|
||||
import { FullAdder } from "./arithmetic/full_adder.js";
|
||||
import { HalfAdder } from "./arithmetic/half_adder.js";
|
||||
import { Inc16 } from "./arithmetic/inc16.js";
|
||||
import { builtinOverrides } from "./builtinOverrides.js";
|
||||
import {
|
||||
Computer,
|
||||
CPU,
|
||||
Keyboard,
|
||||
Memory,
|
||||
ROM32K,
|
||||
Screen,
|
||||
} from "./computer/computer.js";
|
||||
import { And, And16 } from "./logic/and.js";
|
||||
import { DMux, DMux4Way, DMux8Way } from "./logic/dmux.js";
|
||||
import { Mux, Mux4Way16, Mux8Way16, Mux16 } from "./logic/mux.js";
|
||||
import { Nand, Nand16 } from "./logic/nand.js";
|
||||
import { Not, Not16 } from "./logic/not.js";
|
||||
import { Or, Or8way, Or16 } from "./logic/or.js";
|
||||
import { Xor, Xor16 } from "./logic/xor.js";
|
||||
import { Bit, PC, Register, VRegister } from "./sequential/bit.js";
|
||||
import { DFF } from "./sequential/dff.js";
|
||||
import { RAM4K, RAM8, RAM16K, RAM64, RAM512 } from "./sequential/ram.js";
|
||||
|
||||
export {
|
||||
Add16,
|
||||
ALU,
|
||||
And,
|
||||
And16,
|
||||
VRegister as ARegister,
|
||||
Bit,
|
||||
DFF,
|
||||
DMux,
|
||||
VRegister as DRegister,
|
||||
FullAdder,
|
||||
HalfAdder,
|
||||
Inc16,
|
||||
Mux,
|
||||
Mux16,
|
||||
Mux4Way16,
|
||||
Mux8Way16,
|
||||
Nand,
|
||||
Nand16,
|
||||
Not,
|
||||
Not16,
|
||||
Or,
|
||||
Or16,
|
||||
Or8way,
|
||||
RAM16K,
|
||||
RAM4K,
|
||||
RAM512,
|
||||
RAM64,
|
||||
RAM8,
|
||||
Register,
|
||||
Xor,
|
||||
Xor16,
|
||||
};
|
||||
|
||||
export const REGISTRY = new Map<string, () => Chip>(
|
||||
(
|
||||
[
|
||||
["Nand", Nand],
|
||||
["Nand16", Nand16],
|
||||
["Not", Not],
|
||||
["Not16", Not16],
|
||||
["And", And],
|
||||
["And16", And16],
|
||||
["Or", Or],
|
||||
["Or16", Or16],
|
||||
["Or8Way", Or8way],
|
||||
["XOr", Xor],
|
||||
["XOr16", Xor16],
|
||||
["Xor", Xor],
|
||||
["Xor16", Xor16],
|
||||
["Mux", Mux],
|
||||
["Mux16", Mux16],
|
||||
["Mux4Way16", Mux4Way16],
|
||||
["Mux8Way16", Mux8Way16],
|
||||
["DMux", DMux],
|
||||
["DMux4Way", DMux4Way],
|
||||
["DMux8Way", DMux8Way],
|
||||
["HalfAdder", HalfAdder],
|
||||
["FullAdder", FullAdder],
|
||||
["Add16", Add16],
|
||||
["Inc16", Inc16],
|
||||
["ALU", ALU],
|
||||
["ALUNoStat", ALUNoStat],
|
||||
["DFF", DFF],
|
||||
["Bit", Bit],
|
||||
["Register", Register],
|
||||
["ARegister", Register],
|
||||
["DRegister", Register],
|
||||
["PC", PC],
|
||||
["RAM8", RAM8],
|
||||
["RAM64", RAM64],
|
||||
["RAM512", RAM512],
|
||||
["RAM4K", RAM4K],
|
||||
["RAM16K", RAM16K],
|
||||
["ROM32K", ROM32K],
|
||||
["Screen", Screen],
|
||||
["Keyboard", Keyboard],
|
||||
["CPU", CPU],
|
||||
["Computer", Computer],
|
||||
["Memory", Memory],
|
||||
["ARegister", VRegister],
|
||||
["DRegister", VRegister],
|
||||
] as [string, { new (): Chip }][]
|
||||
).map(([name, ChipCtor]) => [
|
||||
name,
|
||||
() => {
|
||||
const chip = new ChipCtor();
|
||||
chip.name = name;
|
||||
return chip;
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export function hasBuiltinChip(name: string): boolean {
|
||||
return REGISTRY.has(name);
|
||||
}
|
||||
|
||||
export async function getBuiltinChip(name: string): Promise<Result<Chip>> {
|
||||
if (builtinOverrides[name]) {
|
||||
const result = await parse(builtinOverrides[name], name);
|
||||
if (isErr(result)) {
|
||||
return Err(new Error(Err(result).message));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const chip = REGISTRY.get(name);
|
||||
return chip
|
||||
? Ok(chip())
|
||||
: Err(new Error(`Chip ${name} not in builtin registry`));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function and(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 && b === 1 ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function and16(a: number, b: number): [number] {
|
||||
return [a & b & 0xffff];
|
||||
}
|
||||
|
||||
export class And extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [n] = and(a, b);
|
||||
this.out().pull(n);
|
||||
}
|
||||
}
|
||||
|
||||
export class And16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [n] = and16(a, b);
|
||||
this.out().busVoltage = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function dmux(inn: Voltage, sel: Voltage): [Voltage, Voltage] {
|
||||
const a = sel === LOW && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === HIGH && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b];
|
||||
}
|
||||
|
||||
export function dmux4way(
|
||||
inn: Voltage,
|
||||
sel: number,
|
||||
): [Voltage, Voltage, Voltage, Voltage] {
|
||||
const a = sel === 0b00 && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === 0b01 && inn === HIGH ? HIGH : LOW;
|
||||
const c = sel === 0b10 && inn === HIGH ? HIGH : LOW;
|
||||
const d = sel === 0b11 && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b, c, d];
|
||||
}
|
||||
|
||||
export function dmux8way(
|
||||
inn: Voltage,
|
||||
sel: number,
|
||||
): [Voltage, Voltage, Voltage, Voltage, Voltage, Voltage, Voltage, Voltage] {
|
||||
const a = sel === 0b000 && inn === HIGH ? HIGH : LOW;
|
||||
const b = sel === 0b001 && inn === HIGH ? HIGH : LOW;
|
||||
const c = sel === 0b010 && inn === HIGH ? HIGH : LOW;
|
||||
const d = sel === 0b011 && inn === HIGH ? HIGH : LOW;
|
||||
const e = sel === 0b100 && inn === HIGH ? HIGH : LOW;
|
||||
const f = sel === 0b101 && inn === HIGH ? HIGH : LOW;
|
||||
const g = sel === 0b110 && inn === HIGH ? HIGH : LOW;
|
||||
const h = sel === 0b111 && inn === HIGH ? HIGH : LOW;
|
||||
return [a, b, c, d, e, f, g, h];
|
||||
}
|
||||
|
||||
export class DMux extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel"], ["a", "b"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").voltage();
|
||||
|
||||
const [a, b] = dmux(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
}
|
||||
}
|
||||
|
||||
export class DMux4Way extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel[2]"], ["a", "b", "c", "d"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").busVoltage;
|
||||
|
||||
const [a, b, c, d] = dmux4way(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
this.out("c").pull(c);
|
||||
this.out("d").pull(d);
|
||||
}
|
||||
}
|
||||
|
||||
export class DMux8Way extends Chip {
|
||||
constructor() {
|
||||
super(["in", "sel[3]"], ["a", "b", "c", "d", "e", "f", "g", "h"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in("in").voltage();
|
||||
const sel = this.in("sel").busVoltage;
|
||||
|
||||
const [a, b, c, d, e, f, g, h] = dmux8way(inn, sel);
|
||||
this.out("a").pull(a);
|
||||
this.out("b").pull(b);
|
||||
this.out("c").pull(c);
|
||||
this.out("d").pull(d);
|
||||
this.out("e").pull(e);
|
||||
this.out("f").pull(f);
|
||||
this.out("g").pull(g);
|
||||
this.out("h").pull(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Chip, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function mux(a: Voltage, b: Voltage, sel: Voltage): [Voltage] {
|
||||
return [sel === LOW ? a : b];
|
||||
}
|
||||
|
||||
export function mux16(a: number, b: number, sel: Voltage): [number] {
|
||||
return [sel === LOW ? a : b];
|
||||
}
|
||||
|
||||
export function mux16_4(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
sel: number,
|
||||
): [number] {
|
||||
const s2 = (sel & 0b01) as Voltage;
|
||||
return (sel & 0b10) === 0b00 ? mux16(a, b, s2) : mux16(c, d, s2);
|
||||
}
|
||||
|
||||
export function mux16_8(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number,
|
||||
g: number,
|
||||
h: number,
|
||||
sel: number,
|
||||
): [number] {
|
||||
const s2 = (sel & 0b11) as Voltage;
|
||||
return (sel & 0b100) === 0b000
|
||||
? mux16_4(a, b, c, d, s2)
|
||||
: mux16_4(e, f, g, h, s2);
|
||||
}
|
||||
|
||||
export class Mux extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b", "sel"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const sel = this.in("sel").voltage();
|
||||
|
||||
const [set] = mux(a, b, sel);
|
||||
this.out().pull(set);
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]", "sel"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const sel = this.in("sel").voltage();
|
||||
const [out] = mux16(a, b, sel);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux4Way16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]", "c[16]", "d[16]", "sel[2]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const c = this.in("c").busVoltage;
|
||||
const d = this.in("d").busVoltage;
|
||||
const sel = this.in("sel").busVoltage;
|
||||
const [out] = mux16_4(a, b, c, d, sel);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Mux8Way16 extends Chip {
|
||||
constructor() {
|
||||
super(
|
||||
[
|
||||
"a[16]",
|
||||
"b[16]",
|
||||
"c[16]",
|
||||
"d[16]",
|
||||
"e[16]",
|
||||
"f[16]",
|
||||
"g[16]",
|
||||
"h[16]",
|
||||
"sel[3]",
|
||||
],
|
||||
["out[16]"],
|
||||
);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const c = this.in("c").busVoltage;
|
||||
const d = this.in("d").busVoltage;
|
||||
const e = this.in("e").busVoltage;
|
||||
const f = this.in("f").busVoltage;
|
||||
const g = this.in("g").busVoltage;
|
||||
const h = this.in("h").busVoltage;
|
||||
const sel = this.in("sel").busVoltage;
|
||||
const [out] = mux16_8(a, b, c, d, e, f, g, h, sel);
|
||||
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { nand16 } from "../../../util/twos.js";
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function nand(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 && b === 1 ? LOW : HIGH];
|
||||
}
|
||||
|
||||
export class Nand extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = nand(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Nand16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
this.out().busVoltage = nand16(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function not(inn: Voltage): [Voltage] {
|
||||
return [inn === LOW ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function not16(inn: number): [number] {
|
||||
return [~inn & 0xffff];
|
||||
}
|
||||
|
||||
export class Not extends Chip {
|
||||
constructor() {
|
||||
super(["in"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("in").voltage();
|
||||
const [out] = not(a);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Not16 extends Chip {
|
||||
constructor() {
|
||||
super(["in[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const [n] = not16(this.in().busVoltage);
|
||||
this.out().busVoltage = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function or(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [a === 1 || b === 1 ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function or16(a: number, b: number): [number] {
|
||||
return [(a | b) & 0xffff];
|
||||
}
|
||||
|
||||
export function or8way(a: number): [Voltage] {
|
||||
return [(a & 0xff) === 0 ? LOW : HIGH];
|
||||
}
|
||||
|
||||
export class Or extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = or(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Or16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = or16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
export class Or8way extends Chip {
|
||||
constructor() {
|
||||
super(["in[8]"], ["out"], "Or8way");
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const [out] = or8way(inn);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Chip, HIGH, LOW, Voltage } from "../../chip.js";
|
||||
|
||||
export function xor(a: Voltage, b: Voltage): [Voltage] {
|
||||
return [(a === HIGH && b === LOW) || (a === LOW && b === HIGH) ? HIGH : LOW];
|
||||
}
|
||||
|
||||
export function xor16(a: number, b: number): [number] {
|
||||
return [(a ^ b) & 0xffff];
|
||||
}
|
||||
|
||||
export class Xor extends Chip {
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").voltage();
|
||||
const b = this.in("b").voltage();
|
||||
const [out] = xor(a, b);
|
||||
this.out().pull(out);
|
||||
}
|
||||
}
|
||||
|
||||
export class Xor16 extends Chip {
|
||||
constructor() {
|
||||
super(["a[16]", "b[16]"], ["out[16]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const a = this.in("a").busVoltage;
|
||||
const b = this.in("b").busVoltage;
|
||||
const [out] = xor16(a, b);
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Bus, ClockedChip, HIGH, LOW, Pin, Voltage } from "../../chip.js";
|
||||
|
||||
export class Bit extends ClockedChip {
|
||||
bit: Voltage = LOW;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(["in", "load"], ["out"], name, [], ["in", "load"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("load").voltage() === HIGH) {
|
||||
this.bit = this.in().voltage();
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().pull(this.bit ?? 0);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bit = LOW;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private register: { bits: number },
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.register.bits & 0xffff;
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.register.bits = num & 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
export class Register extends ClockedChip {
|
||||
bits = 0x00;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(["in[16]", "load"], ["out[16]"], name, [], ["in", "load"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("load").voltage() === HIGH) {
|
||||
this.bits = this.in().busVoltage & 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this.bits & 0xffff;
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
return name === this.name
|
||||
? new RegisterBus(this.name, this)
|
||||
: super.get(name, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bits = 0x00;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class VRegister extends Register {}
|
||||
|
||||
export class PC extends ClockedChip {
|
||||
bits = 0x00;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(
|
||||
["in[16]", "reset", "load", "inc"],
|
||||
["out[16]"],
|
||||
name,
|
||||
[],
|
||||
["in", "reset", "load", "inc"],
|
||||
);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
if (this.in("reset").voltage() === HIGH) {
|
||||
this.bits = 0;
|
||||
} else if (this.in("load").voltage() === HIGH) {
|
||||
this.bits = this.in().busVoltage & 0xffff;
|
||||
} else if (this.in("inc").voltage() === HIGH) {
|
||||
this.bits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this.bits & 0xffff;
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number): Pin | undefined {
|
||||
return name === this.name
|
||||
? new RegisterBus(this.name, this)
|
||||
: super.get(name, offset);
|
||||
}
|
||||
|
||||
override reset() {
|
||||
this.bits = 0x00;
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ClockedChip } from "../../chip.js";
|
||||
|
||||
export class DFF extends ClockedChip {
|
||||
constructor(name?: string) {
|
||||
super(["in"], ["out"], name, ["t"], ["in"]);
|
||||
}
|
||||
|
||||
override tick() {
|
||||
// Read in into t
|
||||
const t = this.in().voltage();
|
||||
this.pin("t").pull(t);
|
||||
}
|
||||
|
||||
override tock() {
|
||||
// write t into out
|
||||
const t = this.pin("t").voltage();
|
||||
this.out().pull(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { Memory, Memory as MemoryChip } from "../../../cpu/memory.js";
|
||||
import { Bus, ClockedChip, Pin } from "../../chip.js";
|
||||
|
||||
export class RAM extends ClockedChip {
|
||||
protected _memory: MemoryChip;
|
||||
private _nextData = 0;
|
||||
private _address = 0;
|
||||
|
||||
get memory() {
|
||||
return this._memory;
|
||||
}
|
||||
get address() {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly width: number,
|
||||
name?: string,
|
||||
) {
|
||||
super(
|
||||
["in[16]", "load", `address[${width}]`],
|
||||
[`out[16]`],
|
||||
name,
|
||||
[],
|
||||
["in", "load"],
|
||||
);
|
||||
this._memory = new MemoryChip(Math.pow(2, this.width));
|
||||
}
|
||||
|
||||
override tick() {
|
||||
const load = this.in("load").voltage();
|
||||
this._address = this.in("address").busVoltage;
|
||||
if (load) {
|
||||
this._nextData = this.in().busVoltage;
|
||||
this._memory.set(this._address, this._nextData);
|
||||
}
|
||||
}
|
||||
|
||||
override tock() {
|
||||
this.out().busVoltage = this._memory?.get(this._address) ?? 0;
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const address = this.in("address").busVoltage;
|
||||
this.out().busVoltage = this._memory?.get(address) ?? 0;
|
||||
}
|
||||
|
||||
at(idx: number): Pin {
|
||||
assert(
|
||||
idx < this._memory.size,
|
||||
() => `Request out of bounds (${idx} >= ${this._memory.size})`,
|
||||
);
|
||||
return new RamBus(`${this.name}[${idx}]`, idx, this._memory);
|
||||
}
|
||||
|
||||
override get(name: string, offset?: number) {
|
||||
return name === this.name ? this.at(offset ?? 0) : super.get(name);
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this._memory.reset();
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class RamBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly index: number,
|
||||
private ram: Memory,
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return this.ram.get(this.index);
|
||||
}
|
||||
|
||||
override set busVoltage(num: number) {
|
||||
this.ram.set(this.index, num);
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM8 extends RAM {
|
||||
constructor() {
|
||||
super(3, "RAM8");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM64 extends RAM {
|
||||
constructor() {
|
||||
super(6, "RAM64");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM512 extends RAM {
|
||||
constructor() {
|
||||
super(9, "RAM512");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM4K extends RAM {
|
||||
constructor() {
|
||||
super(12, "RAM4K");
|
||||
}
|
||||
}
|
||||
|
||||
export class RAM16K extends RAM {
|
||||
constructor() {
|
||||
super(14, "RAM16K");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { And, Inc16, Mux16, Not, Not16, Or, Xor } from "./builtins/index.js";
|
||||
import { Nand } from "./builtins/logic/nand.js";
|
||||
import { Bit, PC, Register } from "./builtins/sequential/bit.js";
|
||||
import { DFF } from "./builtins/sequential/dff.js";
|
||||
import {
|
||||
Bus,
|
||||
Chip,
|
||||
ConstantBus,
|
||||
HIGH,
|
||||
InSubBus,
|
||||
LOW,
|
||||
OutSubBus,
|
||||
parseToPin,
|
||||
printChip,
|
||||
TRUE_BUS,
|
||||
} from "./chip.js";
|
||||
import { Clock } from "./clock.js";
|
||||
|
||||
describe("Chip", () => {
|
||||
it("parses toPin", () => {
|
||||
expect(parseToPin("a")).toMatchObject({ pin: "a" });
|
||||
expect(parseToPin("a[2]")).toMatchObject({ pin: "a", start: 2 });
|
||||
expect(parseToPin("a[2..4]")).toMatchObject({
|
||||
pin: "a",
|
||||
start: 2,
|
||||
end: 4,
|
||||
});
|
||||
});
|
||||
|
||||
describe("combinatorial", () => {
|
||||
describe("nand", () => {
|
||||
it("can eval a nand gate", () => {
|
||||
const nand = new Nand();
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in("a")?.pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
|
||||
nand.in("b")?.pull(HIGH);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(LOW);
|
||||
|
||||
nand.in("a")?.pull(LOW);
|
||||
nand.eval();
|
||||
expect(nand.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("not", () => {
|
||||
it("evaluates a not gate", () => {
|
||||
const notChip = new Not();
|
||||
|
||||
notChip.eval();
|
||||
expect(notChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
notChip.in().pull(HIGH);
|
||||
notChip.eval();
|
||||
expect(notChip.out().voltage()).toBe(LOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and", () => {
|
||||
it("evaluates an and gate", () => {
|
||||
const andChip = new And();
|
||||
|
||||
const a = assertExists(andChip.in("a"));
|
||||
const b = assertExists(andChip.in("b"));
|
||||
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
|
||||
b.pull(HIGH);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
a.pull(LOW);
|
||||
andChip.eval();
|
||||
expect(andChip.out().voltage()).toBe(LOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe("or", () => {
|
||||
it("evaluates an or gate", () => {
|
||||
const orChip = new Or();
|
||||
|
||||
const a = assertExists(orChip.in("a"));
|
||||
const b = assertExists(orChip.in("b"));
|
||||
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
orChip.eval();
|
||||
printChip(orChip);
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
b.pull(HIGH);
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
a.pull(LOW);
|
||||
orChip.eval();
|
||||
expect(orChip.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("xor", () => {
|
||||
it("evaluates an xor gate", () => {
|
||||
const xorChip = new Xor();
|
||||
|
||||
const a = assertExists(xorChip.in("a"));
|
||||
const b = assertExists(xorChip.in("b"));
|
||||
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(HIGH);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(HIGH);
|
||||
|
||||
b.pull(HIGH);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(LOW);
|
||||
|
||||
a.pull(LOW);
|
||||
xorChip.eval();
|
||||
expect(xorChip.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("wide", () => {
|
||||
describe("Not16", () => {
|
||||
it("evaluates a not16 gate", () => {
|
||||
const not16 = new Not16();
|
||||
|
||||
const inn = not16.in();
|
||||
|
||||
inn.busVoltage = 0x0;
|
||||
not16.eval();
|
||||
expect(not16.out().busVoltage).toBe(0xffff);
|
||||
|
||||
inn.busVoltage = 0xf00f;
|
||||
not16.eval();
|
||||
expect(not16.out().busVoltage).toBe(0x0ff0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bus voltage", () => {
|
||||
it("sets and returns wide busses", () => {
|
||||
const pin = new Bus("wide", 16);
|
||||
pin.busVoltage = 0xf00f;
|
||||
expect(pin.voltage(0)).toBe(1);
|
||||
expect(pin.voltage(8)).toBe(0);
|
||||
expect(pin.voltage(9)).toBe(0);
|
||||
expect(pin.voltage(15)).toBe(1);
|
||||
expect(pin.busVoltage).toBe(0xf00f);
|
||||
});
|
||||
|
||||
it("creates wide busses internally", () => {
|
||||
const chip = new Chip([], [], "WithWide");
|
||||
|
||||
chip.wire(new Not16(), [
|
||||
{
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
|
||||
const width = chip.pins.get("a")?.width;
|
||||
expect(width).toBe(16);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and16", () => undefined);
|
||||
});
|
||||
|
||||
describe("SubBus", () => {
|
||||
class Not3 extends Chip {
|
||||
constructor() {
|
||||
super(["in[3]"], ["out[3]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const out = ~inn & 0b111;
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
it("drives OutSubBus", () => {
|
||||
const notChip = new Not();
|
||||
const inPin = new Bus("in", 3);
|
||||
const outSubBus = new OutSubBus(notChip.in(), 1, 1);
|
||||
inPin.connect(outSubBus);
|
||||
|
||||
inPin.busVoltage = 0b0;
|
||||
expect(notChip.in().busVoltage).toBe(0b0);
|
||||
inPin.busVoltage = 0b111;
|
||||
expect(notChip.in().busVoltage).toBe(0b1);
|
||||
});
|
||||
|
||||
it("wires SubBus in=in[1]", () => {
|
||||
const not3Chip = new Not3();
|
||||
const notPart = new Not();
|
||||
const inPin = not3Chip.in();
|
||||
|
||||
not3Chip.wire(notPart, [
|
||||
{
|
||||
from: { name: "in", start: 1, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
|
||||
inPin.busVoltage = 0b0;
|
||||
not3Chip.eval();
|
||||
expect(notPart.in().busVoltage).toBe(0b0);
|
||||
inPin.busVoltage = 0b111;
|
||||
not3Chip.eval();
|
||||
expect(notPart.in().busVoltage).toBe(0b1);
|
||||
});
|
||||
|
||||
it("wires SubBus in[0]=a", () => {
|
||||
const chip = new Chip(["a", "b"], ["out[3]"]);
|
||||
const not3 = new Not3();
|
||||
|
||||
// Not3(in[0]=a, in[1]=b, in[2]=b, out=out)
|
||||
chip.wire(not3, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: undefined },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: undefined },
|
||||
to: { name: "in", start: 1, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: undefined },
|
||||
to: { name: "in", start: 2, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: undefined },
|
||||
to: { name: "out", start: 0, width: undefined },
|
||||
},
|
||||
]);
|
||||
|
||||
assertExists(chip.in("b")).busVoltage = 1;
|
||||
assertExists(chip.in("a")).busVoltage = 0;
|
||||
chip.eval();
|
||||
expect(chip.out().busVoltage).toBe(0b001);
|
||||
});
|
||||
|
||||
it("wires SubBus out=out[1]", () => {
|
||||
const threeChip = new (class ThreeChip extends Chip {
|
||||
constructor() {
|
||||
super([], ["out[3]"]);
|
||||
}
|
||||
})();
|
||||
|
||||
const notPart = new Not();
|
||||
threeChip.wire(notPart, [
|
||||
{
|
||||
from: { name: "out", start: 1, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
const outPin = notPart.out();
|
||||
|
||||
outPin.busVoltage = 0b0;
|
||||
expect(threeChip.out().busVoltage).toBe(0b0);
|
||||
outPin.busVoltage = 0b1;
|
||||
expect(threeChip.out().busVoltage).toBe(0b010);
|
||||
});
|
||||
|
||||
it("widens output busses if necessary", () => {
|
||||
const mux4way16 = new Chip(
|
||||
["in[16]", "b[16]", "c[16]", "d[16]", "sel[2]"],
|
||||
["out[16]"],
|
||||
);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "a", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 0, width: 1 },
|
||||
to: { name: "sel", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out1", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "c", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "d", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 0, width: 1 },
|
||||
to: { name: "sel", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out2", start: 0 },
|
||||
to: { name: "out", start: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
mux4way16.wire(new Mux16(), [
|
||||
{
|
||||
from: { name: "out1", start: 0 },
|
||||
to: { name: "a", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "out2", start: 0 },
|
||||
to: { name: "b", start: 0 },
|
||||
},
|
||||
{
|
||||
from: { name: "sel", start: 1, width: 1 },
|
||||
to: { name: "sel", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("widens internal busses if necessary", () => {
|
||||
const chip = new Chip(["in"], [], "test", ["t"]);
|
||||
|
||||
chip.wire(new Not(), [
|
||||
{
|
||||
from: { name: "in", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "t", start: 1, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
|
||||
chip.in().busVoltage = 0b0;
|
||||
chip.eval();
|
||||
expect(chip.pin("t").busVoltage).toBe(0b10);
|
||||
});
|
||||
|
||||
class Not8 extends Chip {
|
||||
constructor() {
|
||||
super(["in[8]"], ["out[8]"]);
|
||||
}
|
||||
|
||||
override eval() {
|
||||
const inn = this.in().busVoltage;
|
||||
const out = ~inn & 0xff;
|
||||
this.out().busVoltage = out;
|
||||
}
|
||||
}
|
||||
|
||||
it("assigns input inside wide busses", () => {
|
||||
class Foo extends Chip {
|
||||
readonly not8 = new Not8();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.parts.push(this.not8);
|
||||
this.pins.insert(new ConstantBus("pal", 0b1010_1100_0011_0101));
|
||||
this.pins.get("pal")?.connect(new OutSubBus(this.not8.in(), 4, 8));
|
||||
this.pins.emplace("out1", 5);
|
||||
const out1Bus = new OutSubBus(
|
||||
assertExists(this.pins.get("out1")),
|
||||
3,
|
||||
5,
|
||||
);
|
||||
this.not8.out().connect(out1Bus);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
foo.eval();
|
||||
expect(foo.not8.in().busVoltage).toEqual(0b1100_0011);
|
||||
expect(foo.pin("out1")?.busVoltage).toEqual(0b00111);
|
||||
});
|
||||
|
||||
it("assigns output inside wide busses", () => {
|
||||
// From figure A2.2, page 287, 2nd edition
|
||||
class Foo extends Chip {
|
||||
readonly not8 = new Not8();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.parts.push(this.not8);
|
||||
this.pins.insert(new ConstantBus("six", 0b110));
|
||||
// in[0..1] = true
|
||||
TRUE_BUS.connect(new InSubBus(this.not8.in(), 0, 2));
|
||||
// in[3..5] = six, 110
|
||||
this.pins.get("six")?.connect(new InSubBus(this.not8.in(), 3, 3));
|
||||
// in[7] = true
|
||||
TRUE_BUS.connect(new InSubBus(this.not8.in(), 7, 1));
|
||||
// out[3..7] = out1
|
||||
this.pins.emplace("out1", 5);
|
||||
const out1Bus = new OutSubBus(
|
||||
assertExists(this.pins.get("out1")),
|
||||
3,
|
||||
5,
|
||||
);
|
||||
this.not8.out().connect(out1Bus);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
foo.eval();
|
||||
expect(foo.not8.in().busVoltage).toBe(0b10110011);
|
||||
expect(foo.pin("out1").busVoltage).toBe(0b01001);
|
||||
});
|
||||
|
||||
it("pulls portions of true", () => {
|
||||
class Foo extends Chip {
|
||||
readonly chip = new Not3();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.wire(this.chip, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 1, width: 2 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
|
||||
const inVoltage = foo.chip.in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b110));
|
||||
});
|
||||
|
||||
it("pulls start of true", () => {
|
||||
class Foo extends Chip {
|
||||
readonly chip = new Not3();
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.wire(this.chip, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 2 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
|
||||
const inVoltage = foo.chip.in().busVoltage;
|
||||
expect(bin(inVoltage)).toBe(bin(0b11));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sequential", () => {
|
||||
const clock = Clock.get();
|
||||
beforeEach(() => {
|
||||
clock.reset();
|
||||
});
|
||||
|
||||
describe("dff", () => {
|
||||
it("flips and flops", () => {
|
||||
clock.reset();
|
||||
const dff = new DFF();
|
||||
|
||||
clock.tick(); // Read input, low
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
clock.tock(); // Write t, low
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
|
||||
dff.in().pull(HIGH);
|
||||
clock.tick(); // Read input, HIGH
|
||||
expect(dff.out().voltage()).toBe(LOW);
|
||||
clock.tock(); // Write t, HIGH
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
|
||||
clock.tick();
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
clock.tock();
|
||||
expect(dff.out().voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bit", () => {
|
||||
it("does not update when load is low", () => {
|
||||
clock.reset();
|
||||
const bit = new Bit();
|
||||
|
||||
const inn = bit.in();
|
||||
const load = bit.in("load");
|
||||
const out = bit.out();
|
||||
|
||||
load.pull(LOW);
|
||||
inn.pull(HIGH);
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tick();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tock();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
});
|
||||
|
||||
it("does updates when load is high", () => {
|
||||
clock.reset();
|
||||
const bit = new Bit();
|
||||
|
||||
const inn = bit.in();
|
||||
const load = bit.in("load");
|
||||
const out = bit.out();
|
||||
|
||||
load.pull(HIGH);
|
||||
inn.pull(HIGH);
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tick();
|
||||
expect(out.voltage()).toBe(LOW);
|
||||
clock.tock();
|
||||
expect(out.voltage()).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PC", () => {
|
||||
it("remains constant when not ticking", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
});
|
||||
|
||||
it("increments when ticking", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
pc.in("inc").pull(HIGH);
|
||||
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(1);
|
||||
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(1);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(2);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
clock.eval();
|
||||
expect(out.busVoltage).toBe(i + 3);
|
||||
}
|
||||
});
|
||||
|
||||
it("loads a jump value", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
|
||||
pc.in().busVoltage = 0x8286;
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tick();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.tock();
|
||||
expect(out.busVoltage).toBe(0);
|
||||
|
||||
pc.in("load").pull(HIGH);
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
clock.eval();
|
||||
expect(out.busVoltage).toBe(0x8286);
|
||||
});
|
||||
|
||||
it("resets", () => {
|
||||
clock.reset();
|
||||
const pc = new PC();
|
||||
const out = pc.out();
|
||||
pc.in("inc").pull(HIGH);
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
clock.eval();
|
||||
}
|
||||
|
||||
expect(out.busVoltage).toBe(10);
|
||||
|
||||
pc.in("reset").pull(HIGH);
|
||||
|
||||
clock.eval();
|
||||
|
||||
expect(out.busVoltage).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sorts parts before eval", () => {
|
||||
class FooA extends Chip {
|
||||
readonly notA = new Not();
|
||||
readonly notB = new Not();
|
||||
constructor() {
|
||||
super([], ["out"], "Foo", ["x"]);
|
||||
this.wire(this.notA, [
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notB, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const fooA = new FooA();
|
||||
fooA.sortParts();
|
||||
expect(fooA.parts).toEqual([fooA.notB, fooA.notA]);
|
||||
|
||||
class FooB extends Chip {
|
||||
readonly notA = new Not();
|
||||
readonly notB = new Not();
|
||||
constructor() {
|
||||
super([], ["out"], "Foo", ["x"]);
|
||||
this.wire(this.notA, [
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notB, [
|
||||
{
|
||||
from: { name: "x", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const fooB = new FooB();
|
||||
fooB.sortParts();
|
||||
expect(fooB.parts).toEqual([fooB.notA, fooB.notB]);
|
||||
});
|
||||
|
||||
it("sorts clocked chips", () => {
|
||||
class FooC extends Chip {
|
||||
readonly register = new Register();
|
||||
readonly inc16A = new Inc16();
|
||||
readonly inc16B = new Inc16();
|
||||
constructor() {
|
||||
super([], [], "Foo", []);
|
||||
this.wire(this.inc16B, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "c", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.register, [
|
||||
{
|
||||
from: { name: "c", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "true", start: 0, width: 1 },
|
||||
to: { name: "load", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.inc16A, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 16 },
|
||||
to: { name: "in", start: 0, width: 16 },
|
||||
},
|
||||
{
|
||||
from: { name: "b", start: 0, width: 16 },
|
||||
to: { name: "out", start: 0, width: 16 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
const fooC = new FooC();
|
||||
fooC.sortParts();
|
||||
const parts = fooC.parts.map((chip) => chip.id);
|
||||
expect(parts).toEqual([fooC.register.id, fooC.inc16A.id, fooC.inc16B.id]);
|
||||
});
|
||||
|
||||
it("evals without order issues (after sorting)", () => {
|
||||
/*
|
||||
CHIP Or {
|
||||
IN a, b;
|
||||
OUT out;
|
||||
|
||||
PARTS:
|
||||
|
||||
Not(in =b , out = net2);
|
||||
Nand(a = net, b =net2 , out =out );
|
||||
Not(in =a , out = net);
|
||||
}
|
||||
*/
|
||||
class OrA extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrA", []);
|
||||
this.wire(this.nota, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.nand, [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notb, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.sortParts();
|
||||
}
|
||||
}
|
||||
|
||||
class OrB extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrB", []);
|
||||
this.wire(this.nota, [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.notb, [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
this.wire(this.nand, [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class OrC extends Chip {
|
||||
readonly nota = new Not();
|
||||
readonly nand = new Nand();
|
||||
readonly notb = new Not();
|
||||
constructor() {
|
||||
super(["a", "b"], ["out"], "OrC", []);
|
||||
this.wireAll([
|
||||
{
|
||||
part: this.nota,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "b", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
part: this.nand,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "a", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net2", start: 0, width: 1 },
|
||||
to: { name: "b", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "out", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
part: this.notb,
|
||||
connections: [
|
||||
{
|
||||
from: { name: "a", start: 0, width: 1 },
|
||||
to: { name: "in", start: 0, width: 1 },
|
||||
},
|
||||
{
|
||||
from: { name: "net", start: 0, width: 1 },
|
||||
to: { name: "out", start: 0, width: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const ora = new OrA();
|
||||
ora.in("a").pull(HIGH);
|
||||
ora.in("b").pull(LOW);
|
||||
ora.eval();
|
||||
expect(ora.out("out").busVoltage).toBe(HIGH);
|
||||
|
||||
const orb = new OrB();
|
||||
orb.in("a").pull(HIGH);
|
||||
orb.in("b").pull(LOW);
|
||||
orb.eval();
|
||||
expect(orb.out("out").busVoltage).toBe(HIGH);
|
||||
|
||||
const orc = new OrC();
|
||||
orc.in("a").pull(HIGH);
|
||||
orc.in("b").pull(LOW);
|
||||
orc.eval();
|
||||
expect(orc.out("out").busVoltage).toBe(HIGH);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,830 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { assert, assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import type { Subscription } from "rxjs";
|
||||
import { bin } from "../util/twos.js";
|
||||
import { Clock } from "./clock.js";
|
||||
|
||||
export const HIGH = 1;
|
||||
export const LOW = 0;
|
||||
export type Voltage = typeof HIGH | typeof LOW;
|
||||
|
||||
export interface Pin {
|
||||
readonly name: string;
|
||||
readonly width: number;
|
||||
busVoltage: number;
|
||||
pull(voltage: Voltage, bit?: number): void;
|
||||
toggle(bit?: number): void;
|
||||
voltage(bit?: number): Voltage;
|
||||
connect(pin: Pin): void;
|
||||
}
|
||||
|
||||
export function isConstantPin(pinName: string): boolean {
|
||||
return (
|
||||
pinName === "false" ||
|
||||
pinName === "true" ||
|
||||
pinName === "0" ||
|
||||
pinName === "1"
|
||||
);
|
||||
}
|
||||
|
||||
export class Bus implements Pin {
|
||||
state: Voltage[];
|
||||
next: Pin[] = [];
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly width = 1,
|
||||
) {
|
||||
this.state = range(0, this.width).map(() => LOW);
|
||||
}
|
||||
|
||||
ensureWidth(newWidth: number) {
|
||||
assert(newWidth <= 16, `Cannot widen past 16 to ${newWidth} bits`);
|
||||
if (this.width < newWidth) {
|
||||
(this as { width: number }).width = newWidth;
|
||||
this.state = [
|
||||
...this.state,
|
||||
...range(this.width, newWidth).map(() => LOW as Voltage),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
connect(next: Pin) {
|
||||
this.next.push(next);
|
||||
next.busVoltage = this.busVoltage;
|
||||
}
|
||||
|
||||
pull(voltage: Voltage, bit = 0) {
|
||||
assert(
|
||||
bit >= 0 && bit < this.width,
|
||||
`Bit out of bounds: ${this.name}@${bit}`,
|
||||
);
|
||||
this.state[bit] = voltage;
|
||||
this.next.forEach((n) => n.pull(voltage, bit));
|
||||
}
|
||||
|
||||
voltage(bit = 0): Voltage {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
return this.state[bit];
|
||||
}
|
||||
|
||||
set busVoltage(voltage: number) {
|
||||
for (const i of range(0, this.width)) {
|
||||
this.state[i] = ((voltage & (1 << i)) >> i) as Voltage;
|
||||
}
|
||||
this.next.forEach((n) => (n.busVoltage = this.busVoltage));
|
||||
}
|
||||
|
||||
get busVoltage(): number {
|
||||
return range(0, this.width).reduce((b, i) => b | (this.state[i] << i), 0);
|
||||
}
|
||||
|
||||
toggle(bit = 0) {
|
||||
const nextVoltage = this.voltage(bit) === LOW ? HIGH : LOW;
|
||||
this.pull(nextVoltage, bit);
|
||||
}
|
||||
}
|
||||
|
||||
export class InSubBus extends Bus {
|
||||
constructor(
|
||||
private bus: Pin,
|
||||
private start: number,
|
||||
override readonly width = 1,
|
||||
) {
|
||||
super(bus.name);
|
||||
assert(
|
||||
start >= 0 && start + width <= bus.width,
|
||||
`Mismatched InSubBus dimensions on ${bus.name} (${width} + ${start} > ${bus.width})`,
|
||||
);
|
||||
this.connect(bus);
|
||||
}
|
||||
|
||||
override pull(voltage: Voltage, bit = 0) {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
this.bus.pull(voltage, this.start + bit);
|
||||
}
|
||||
|
||||
override voltage(bit = 0): Voltage {
|
||||
assert(bit >= 0 && bit < this.width);
|
||||
return this.bus.voltage(this.start + bit);
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
const high = this.bus.busVoltage & ~mask(this.width + this.start);
|
||||
const low = this.bus.busVoltage & mask(this.start);
|
||||
const mid = (voltage & mask(this.width)) << this.start;
|
||||
this.bus.busVoltage = high | mid | low;
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return (this.bus.busVoltage >> this.start) & mask(this.width);
|
||||
}
|
||||
|
||||
override connect(bus: Pin): void {
|
||||
assert(
|
||||
this.start + this.width <= bus.width,
|
||||
`Mismatched InSubBus connection dimensions (From ${bus.name} to ${this.name})`,
|
||||
);
|
||||
this.bus = bus;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutSubBus extends Bus {
|
||||
constructor(
|
||||
private bus: Pin,
|
||||
private start: number,
|
||||
override readonly width = 1,
|
||||
) {
|
||||
super(bus.name);
|
||||
assert(start >= 0 && width <= bus.width, `Mismatched OutSubBus dimensions`);
|
||||
this.connect(bus);
|
||||
}
|
||||
|
||||
override pull(voltage: Voltage, bit = 0) {
|
||||
if (bit >= this.start && bit < this.start + this.width) {
|
||||
this.bus.pull(voltage, bit - this.start);
|
||||
}
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
this.bus.busVoltage =
|
||||
(voltage & mask(this.width + this.start)) >> this.start;
|
||||
}
|
||||
|
||||
override get busVoltage(): number {
|
||||
return (this.bus.busVoltage >> this.start) & mask(this.width);
|
||||
}
|
||||
|
||||
override connect(bus: Pin): void {
|
||||
assert(
|
||||
this.width <= bus.width,
|
||||
`Mismatched OutSubBus connection dimensions`,
|
||||
);
|
||||
this.bus = bus;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConstantBus extends Bus {
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly value: number,
|
||||
) {
|
||||
super(name, 16 /* TODO: get high bit index */);
|
||||
}
|
||||
|
||||
pullHigh(_ = 0) {
|
||||
return undefined;
|
||||
}
|
||||
pullLow(_ = 0) {
|
||||
return undefined;
|
||||
}
|
||||
override voltage(_ = 0): Voltage {
|
||||
return (this.busVoltage & 0x1) as Voltage;
|
||||
}
|
||||
|
||||
override set busVoltage(voltage: number) {
|
||||
// Noop
|
||||
}
|
||||
override get busVoltage(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export const TRUE_BUS = new ConstantBus("true", 0xffff);
|
||||
export const FALSE_BUS = new ConstantBus("false", 0);
|
||||
|
||||
export function parsePinDecl(toPin: string): {
|
||||
pin: string;
|
||||
width: number;
|
||||
} {
|
||||
const { pin, w } = toPin.match(/(?<pin>[a-zA-Z]+)(\[(?<w>\d+)\])?/)
|
||||
?.groups as {
|
||||
pin: string;
|
||||
w?: string;
|
||||
};
|
||||
return {
|
||||
pin,
|
||||
width: w ? Number(w) : 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseToPin(toPin: string): {
|
||||
pin: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
} {
|
||||
const { pin, i, j } = toPin.match(
|
||||
/(?<pin>[a-z]+)(\[(?<i>\d+)(\.\.(?<j>\d+))?\])?/,
|
||||
)?.groups as { pin: string; i?: string; j?: string };
|
||||
return {
|
||||
pin,
|
||||
start: i ? Number(i) : undefined,
|
||||
end: j ? Number(j) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export class Pins {
|
||||
private readonly map = new Map<string, Pin>();
|
||||
|
||||
insert(pin: Pin) {
|
||||
const { name } = pin;
|
||||
assert(!this.map.has(name), `Pins already has ${name}!`);
|
||||
this.map.set(name, pin);
|
||||
}
|
||||
|
||||
emplace(name: string, minWidth?: number) {
|
||||
if (this.has(name)) {
|
||||
return assertExists(this.get(name));
|
||||
} else {
|
||||
const pin = new Bus(name, minWidth);
|
||||
this.insert(pin);
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
|
||||
has(pin: string): boolean {
|
||||
return this.map.has(pin);
|
||||
}
|
||||
|
||||
get(pin: string): Pin | undefined {
|
||||
return this.map.get(pin);
|
||||
}
|
||||
|
||||
entries(): Iterable<Pin> {
|
||||
return this.map.values();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
function validateWidth(
|
||||
start: number,
|
||||
width: number,
|
||||
pin: Pin,
|
||||
): Result<void, string> {
|
||||
return start + width <= pin.width
|
||||
? Ok()
|
||||
: Err(`Sub-bus index out of range (${pin.name} has width ${pin.width})`);
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
|
||||
export interface PartWireError {
|
||||
wireIndex: number;
|
||||
lhs: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WireError {
|
||||
message: string;
|
||||
lhs: boolean;
|
||||
}
|
||||
export class Chip {
|
||||
readonly id = id++;
|
||||
ins = new Pins();
|
||||
outs = new Pins();
|
||||
pins = new Pins();
|
||||
insToPart = new Map<string, Set<Chip>>();
|
||||
partToOuts = new Map<Chip, Set<string>>();
|
||||
parts: Chip[] = [];
|
||||
clockedPins: Set<string>;
|
||||
clockSubscription?: Subscription;
|
||||
|
||||
get clocked() {
|
||||
if (this.clockedPins.size > 0) {
|
||||
return true;
|
||||
}
|
||||
for (const part of this.parts) {
|
||||
if (part.clocked) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor(
|
||||
ins: (string | { pin: string; width: number })[],
|
||||
outs: (string | { pin: string; width: number })[],
|
||||
public name?: string,
|
||||
internals: (string | { pin: string; width: number })[] = [],
|
||||
clocked: string[] = [],
|
||||
) {
|
||||
for (const inn of ins) {
|
||||
const { pin, width = 1 } =
|
||||
(inn as { pin: string }).pin !== undefined
|
||||
? (inn as { pin: string; width: number })
|
||||
: parsePinDecl(inn as string);
|
||||
this.ins.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
for (const out of outs) {
|
||||
const { pin, width = 1 } =
|
||||
(out as { pin: string }).pin !== undefined
|
||||
? (out as { pin: string; width: number })
|
||||
: parsePinDecl(out as string);
|
||||
this.outs.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
for (const internal of internals) {
|
||||
const { pin, width = 1 } =
|
||||
(internal as { pin: string }).pin !== undefined
|
||||
? (internal as { pin: string; width: number })
|
||||
: parsePinDecl(internal as string);
|
||||
this.pins.insert(new Bus(pin, width));
|
||||
}
|
||||
|
||||
this.clockedPins = new Set(clocked);
|
||||
|
||||
this.subscribeToClock();
|
||||
}
|
||||
|
||||
subscribeToClock() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
this.clockSubscription = Clock.subscribe(() => this.eval());
|
||||
}
|
||||
|
||||
reset() {
|
||||
for (const [_, pin] of this.ins) {
|
||||
pin.busVoltage = 0;
|
||||
}
|
||||
for (const part of this.parts) {
|
||||
part.reset();
|
||||
}
|
||||
this.eval();
|
||||
}
|
||||
|
||||
in(pin = "in"): Pin {
|
||||
assert(this.hasIn(pin), `No in pin ${pin}`);
|
||||
return assertExists(this.ins.get(pin));
|
||||
}
|
||||
|
||||
out(pin = "out"): Pin {
|
||||
assert(this.hasOut(pin), `No in pin ${pin}`);
|
||||
return assertExists(this.outs.get(pin));
|
||||
}
|
||||
|
||||
hasIn(pin: string): boolean {
|
||||
return this.ins.has(pin);
|
||||
}
|
||||
|
||||
hasOut(pin: string): boolean {
|
||||
return this.outs.has(pin);
|
||||
}
|
||||
|
||||
pin(name: string): Pin {
|
||||
assert(this.pins.has(name), "Pin not available in ");
|
||||
return assertExists(this.pins.get(name));
|
||||
}
|
||||
|
||||
get(name: string, offset?: number): Pin | undefined {
|
||||
if (this.ins.has(name)) {
|
||||
return assertExists(this.ins.get(name));
|
||||
}
|
||||
if (this.outs.has(name)) {
|
||||
return assertExists(this.outs.get(name));
|
||||
}
|
||||
if (this.pins.has(name)) {
|
||||
return assertExists(this.pins.get(name));
|
||||
}
|
||||
return this.getBuiltin(name, offset);
|
||||
}
|
||||
|
||||
private getBuiltin(name: string, offset = 0): Pin | undefined {
|
||||
if (BUILTIN_NAMES.includes(name)) {
|
||||
for (const part of this.parts) {
|
||||
const pin = part.get(name, offset);
|
||||
if (pin) {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isInPin(pin: string): boolean {
|
||||
return this.ins.has(pin);
|
||||
}
|
||||
|
||||
isOutPin(pin: string): boolean {
|
||||
return this.outs.has(pin);
|
||||
}
|
||||
|
||||
isExternalPin(pin: string): boolean {
|
||||
return this.isInPin(pin) || this.isOutPin(pin) || isConstantPin(pin);
|
||||
}
|
||||
|
||||
isInternalPin(pin: string): boolean {
|
||||
return !this.isExternalPin(pin);
|
||||
}
|
||||
|
||||
pathExists(start: string, end: string) {
|
||||
const nodes: (Chip | string)[] = [start];
|
||||
|
||||
while (nodes.length > 0) {
|
||||
const node = assertExists(nodes.pop());
|
||||
if (typeof node == "string") {
|
||||
if (node == end) {
|
||||
return true;
|
||||
}
|
||||
nodes.push(...(this.insToPart.get(node) ?? []));
|
||||
} else {
|
||||
nodes.push(...(this.partToOuts.get(node) ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isClockedPin(pin: string) {
|
||||
if (this.isInPin(pin)) {
|
||||
return ![...this.outs].some(([out, _]) => this.pathExists(pin, out));
|
||||
} else {
|
||||
return ![...this.ins].some(([in_, _]) => this.pathExists(in_, pin));
|
||||
}
|
||||
}
|
||||
|
||||
hasConnection(from: Chip, to: Chip): boolean {
|
||||
return [...(this.partToOuts.get(from) ?? [])].some((pin) =>
|
||||
this.insToPart.get(pin)?.has(to),
|
||||
);
|
||||
}
|
||||
|
||||
wire(part: Chip, connections: Connection[]): Result<void, PartWireError> {
|
||||
for (let i = 0; i < connections.length; i++) {
|
||||
const { from, to } = connections[i];
|
||||
if (part.isOutPin(to.name)) {
|
||||
const result = this.wireOutPin(part, to, from);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
wireIndex: i,
|
||||
lhs: Err(result).lhs,
|
||||
message: Err(result).message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const result = this.wireInPin(part, to, from);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
wireIndex: i,
|
||||
lhs: Err(result).lhs,
|
||||
message: Err(result).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.parts.push(part);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
wireAll(wires: Iterable<{ part: Chip; connections: Connection[] }>) {
|
||||
for (const { part, connections } of wires) {
|
||||
this.wire(part, connections);
|
||||
}
|
||||
this.sortParts();
|
||||
}
|
||||
|
||||
// Returns whether the part connection graph has a loop. This should be called
|
||||
// after wiring pins, so that connections are sorted topologically to best
|
||||
// simulate non-order-dependent wiring. This can be handled manually (OrB),
|
||||
// by calling sortParts() after wiring (OrA), or by using wireAll for creating
|
||||
// wires (OrC).
|
||||
sortParts(): boolean {
|
||||
const sorted: Chip[] = [];
|
||||
const visited = new Set<Chip>();
|
||||
const visiting = new Set<Chip>();
|
||||
|
||||
type Node = { part: Chip; isReturning: boolean };
|
||||
|
||||
const stack: Node[] = this.parts.map((part) => ({
|
||||
part,
|
||||
isReturning: false,
|
||||
}));
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = assertExists(stack.pop());
|
||||
|
||||
if (node.isReturning) {
|
||||
// If we are returning to this node, we can safely add it to the sorted list
|
||||
visited.add(node.part);
|
||||
sorted.push(node.part);
|
||||
} else if (!visited.has(node.part)) {
|
||||
if (visiting.has(node.part)) {
|
||||
return true;
|
||||
}
|
||||
visiting.add(node.part);
|
||||
|
||||
// Re-push this node to handle it on return
|
||||
stack.push({ part: node.part, isReturning: true });
|
||||
|
||||
// Push all its children to visit them
|
||||
for (const out of this.partToOuts.get(node.part) ?? []) {
|
||||
stack.push(
|
||||
...Array.from(this.insToPart.get(out) ?? [])
|
||||
.filter((part) => !visited.has(part))
|
||||
.map((part) => ({
|
||||
part,
|
||||
isReturning: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parts = sorted.reverse();
|
||||
return false;
|
||||
}
|
||||
|
||||
private findPin(from: string, minWidth?: number): Pin {
|
||||
if (from === "true" || from === "1") {
|
||||
return TRUE_BUS;
|
||||
}
|
||||
if (from === "false" || from === "0") {
|
||||
return FALSE_BUS;
|
||||
}
|
||||
if (this.ins.has(from)) {
|
||||
return assertExists(this.ins.get(from));
|
||||
}
|
||||
if (this.outs.has(from)) {
|
||||
return assertExists(this.outs.get(from));
|
||||
}
|
||||
return this.pins.emplace(from, minWidth);
|
||||
}
|
||||
|
||||
private wireOutPin(
|
||||
part: Chip,
|
||||
to: PinSide,
|
||||
from: PinSide,
|
||||
): Result<void, WireError> {
|
||||
const partPin = assertExists(
|
||||
part.outs.get(to.name),
|
||||
() => `Cannot wire to missing pin ${to.name}`,
|
||||
);
|
||||
to.width ??= partPin.width;
|
||||
|
||||
let chipPin = this.findPin(from.name, from.width ?? to.width);
|
||||
const isInternal = this.pins.has(chipPin.name);
|
||||
|
||||
from.width ??= chipPin.width;
|
||||
|
||||
if (chipPin instanceof ConstantBus) {
|
||||
return Err({
|
||||
message: `Cannot wire to constant bus`,
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Widen internal pins
|
||||
if (isInternal && chipPin instanceof Bus) {
|
||||
chipPin.ensureWidth(from.start + from.width);
|
||||
}
|
||||
|
||||
// Wrap the chipPin in an InBus when the chip side is dimensioned
|
||||
if (from.start > 0 || from.width !== chipPin.width) {
|
||||
const result = validateWidth(from.start, from.width, chipPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
chipPin = new InSubBus(chipPin, from.start, from.width);
|
||||
}
|
||||
|
||||
// Wrap the chipPin in an OutBus when the part side is dimensioned
|
||||
if (to.start > 0 || to.width !== partPin.width) {
|
||||
const result = validateWidth(to.start, to.width, partPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: false,
|
||||
});
|
||||
}
|
||||
chipPin = new OutSubBus(chipPin, to.start, to.width);
|
||||
}
|
||||
|
||||
if (!part.clockedPins.has(partPin.name)) {
|
||||
const partToOuts = this.partToOuts.get(part) ?? new Set();
|
||||
partToOuts.add(chipPin.name);
|
||||
this.partToOuts.set(part, partToOuts);
|
||||
}
|
||||
|
||||
const loop = this.sortParts();
|
||||
|
||||
if (loop) {
|
||||
const partToOuts = this.partToOuts.get(part) ?? new Set();
|
||||
partToOuts.delete(chipPin.name);
|
||||
this.partToOuts.set(part, partToOuts);
|
||||
return Err({ message: "Circular pin dependency", lhs: false });
|
||||
} else {
|
||||
partPin.connect(chipPin);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private wireInPin(
|
||||
part: Chip,
|
||||
to: PinSide,
|
||||
from: PinSide,
|
||||
): Result<void, WireError> {
|
||||
let partPin = assertExists(
|
||||
part.ins.get(to.name),
|
||||
() => `Cannot wire to missing pin ${to.name}`,
|
||||
);
|
||||
to.width ??= partPin.width;
|
||||
|
||||
const chipPin = this.findPin(from.name, from.width ?? to.width);
|
||||
|
||||
from.width ??= chipPin.width;
|
||||
|
||||
// Wrap the partPin in an InBus when the part side is dimensioned
|
||||
if (to.start > 0 || to.width !== partPin.width) {
|
||||
const result = validateWidth(to.start, to.width, partPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: true,
|
||||
});
|
||||
}
|
||||
partPin = new InSubBus(partPin, to.start, to.width);
|
||||
}
|
||||
|
||||
// Wrap the partPin in an OutBus when the chip side is dimensioned
|
||||
if (!["true", "false"].includes(chipPin.name)) {
|
||||
if (from.start > 0 || from.width !== chipPin.width) {
|
||||
const result = validateWidth(from.start, from.width, chipPin);
|
||||
if (isErr(result)) {
|
||||
return Err({
|
||||
message: Err(result),
|
||||
lhs: false,
|
||||
});
|
||||
}
|
||||
partPin = new OutSubBus(partPin, from.start, from.width);
|
||||
}
|
||||
}
|
||||
|
||||
if (!part.clockedPins.has(partPin.name)) {
|
||||
const pinsToPart = this.insToPart.get(chipPin.name) ?? new Set();
|
||||
pinsToPart.add(part);
|
||||
this.insToPart.set(chipPin.name, pinsToPart);
|
||||
}
|
||||
|
||||
const loop = this.sortParts();
|
||||
|
||||
if (loop) {
|
||||
const pinsToPart = this.insToPart.get(chipPin.name) ?? new Set();
|
||||
pinsToPart.delete(part);
|
||||
this.insToPart.set(chipPin.name, pinsToPart);
|
||||
return Err({ message: "Circular pin dependency", lhs: true });
|
||||
} else {
|
||||
chipPin.connect(partPin);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
eval() {
|
||||
for (const chip of this.parts) {
|
||||
TRUE_BUS.next.forEach((pin) => (pin.busVoltage = TRUE_BUS.busVoltage));
|
||||
FALSE_BUS.next.forEach((pin) => (pin.busVoltage = FALSE_BUS.busVoltage));
|
||||
chip.eval();
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
tock() {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
for (const part of this.parts) {
|
||||
part.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// For the ROM32K builtin to load from a file system
|
||||
async load(fs: FileSystem, path: string): Promise<void> {
|
||||
for (const part of this.parts) {
|
||||
if (part.name === "ROM32K") {
|
||||
await part.load(fs, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Low extends Chip {
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.outs.insert(FALSE_BUS);
|
||||
}
|
||||
}
|
||||
|
||||
export class High extends Chip {
|
||||
constructor() {
|
||||
super([], []);
|
||||
this.outs.insert(TRUE_BUS);
|
||||
}
|
||||
}
|
||||
|
||||
export class ClockedChip extends Chip {
|
||||
override get clocked(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override subscribeToClock(): void {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
this.clockSubscription = Clock.subscribe(({ level }) => {
|
||||
if (level === LOW) {
|
||||
this.tock();
|
||||
} else {
|
||||
this.tick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override remove() {
|
||||
this.clockSubscription?.unsubscribe();
|
||||
super.remove();
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
super.reset();
|
||||
this.tick();
|
||||
this.tock();
|
||||
}
|
||||
}
|
||||
|
||||
export interface PinSide {
|
||||
name: string;
|
||||
start: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
// To is the part side
|
||||
to: PinSide;
|
||||
// From is the chip side
|
||||
from: PinSide;
|
||||
}
|
||||
|
||||
export type Pinout = Record<string, string>;
|
||||
export interface SerializedChip {
|
||||
id: number;
|
||||
name: string;
|
||||
ins: Pinout;
|
||||
outs: Pinout;
|
||||
pins: Pinout;
|
||||
children: SerializedChip[];
|
||||
}
|
||||
|
||||
function mask(width: number) {
|
||||
return Math.pow(2, width) - 1;
|
||||
}
|
||||
|
||||
function setBus(busses: Pinout, pin: Pin) {
|
||||
busses[pin.name] = bin(
|
||||
(pin.busVoltage & mask(pin.width)) <<
|
||||
(pin as unknown as { start: number }).start,
|
||||
);
|
||||
return busses;
|
||||
}
|
||||
|
||||
export function printChip(chip: Chip): SerializedChip {
|
||||
return {
|
||||
id: chip.id,
|
||||
name: chip.name ?? chip.constructor.name,
|
||||
ins: [...chip.ins.entries()].reduce(setBus, {} as Pinout),
|
||||
outs: [...chip.outs.entries()].reduce(setBus, {} as Pinout),
|
||||
pins: [...chip.pins.entries()].reduce(setBus, {} as Pinout),
|
||||
children: [...chip.parts.values()].map(printChip),
|
||||
};
|
||||
}
|
||||
|
||||
export const BUILTIN_NAMES = [
|
||||
"Register",
|
||||
"ARegister",
|
||||
"DRegister",
|
||||
"PC",
|
||||
"RAM8",
|
||||
"RAM64",
|
||||
"RAM512",
|
||||
"RAM4K",
|
||||
"RAM16K",
|
||||
"ROM32K",
|
||||
"Screen",
|
||||
"Keyboard",
|
||||
"Memory",
|
||||
];
|
||||
@@ -0,0 +1,92 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
||||
import { HIGH, LOW, Voltage } from "./chip.js";
|
||||
|
||||
interface Tick {
|
||||
readonly level: Voltage;
|
||||
readonly ticks: number;
|
||||
}
|
||||
|
||||
let clock: Clock;
|
||||
export class Clock {
|
||||
private level: Voltage = LOW;
|
||||
private ticks = 0;
|
||||
|
||||
static get() {
|
||||
if (clock === undefined) {
|
||||
clock = new Clock();
|
||||
}
|
||||
return clock;
|
||||
}
|
||||
|
||||
static subscribe(observer: (value: Tick) => void) {
|
||||
return Clock.get().$.subscribe(observer);
|
||||
}
|
||||
|
||||
get isHigh(): boolean {
|
||||
return this.level === HIGH;
|
||||
}
|
||||
|
||||
get isLow(): boolean {
|
||||
return this.level === LOW;
|
||||
}
|
||||
|
||||
private subject = new BehaviorSubject<Tick>({
|
||||
level: this.level,
|
||||
ticks: this.ticks,
|
||||
});
|
||||
readonly frameSubject = new Subject<void>();
|
||||
readonly resetSubject = new Subject<void>();
|
||||
|
||||
readonly $: Observable<Tick> = this.subject;
|
||||
readonly frame$: Observable<void> = this.frameSubject;
|
||||
readonly reset$: Observable<void> = this.resetSubject;
|
||||
|
||||
private next() {
|
||||
this.subject.next({
|
||||
level: this.level,
|
||||
ticks: this.ticks,
|
||||
});
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
// private
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.level = LOW;
|
||||
this.ticks = 0;
|
||||
this.next();
|
||||
this.resetSubject.next();
|
||||
}
|
||||
|
||||
tick() {
|
||||
assert(this.level === LOW, "Can only tick up from LOW");
|
||||
this.level = HIGH;
|
||||
this.next();
|
||||
}
|
||||
|
||||
tock() {
|
||||
assert(this.level === HIGH, "Can only tock down from HIGH");
|
||||
this.level = LOW;
|
||||
this.ticks += 1;
|
||||
this.next();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.level === HIGH ? this.tock() : this.tick();
|
||||
}
|
||||
|
||||
eval() {
|
||||
this.tick();
|
||||
this.tock();
|
||||
}
|
||||
|
||||
frame() {
|
||||
this.frameSubject.next();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.ticks}${this.level === HIGH ? "+" : ""}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
These are the errors from the current builder.
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L212
|
||||
try {
|
||||
result = getSubBus(pinName);
|
||||
} catch (Exception e) {
|
||||
input.HDLError(pinName + " has an invalid sub bus specification");
|
||||
}
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L217
|
||||
if (result != null) {
|
||||
if (result[0] < 0 || result[1] < 0)
|
||||
input.HDLError(pinName + ": negative bit numbers are illegal");
|
||||
else if (result[0] > result[1])
|
||||
input.HDLError(pinName + ": left bit number should be lower than the right one");
|
||||
else if (result[1] >= busWidth)
|
||||
input.HDLError(pinName + ": the specified sub bus is not in the bus range");
|
||||
}
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L274
|
||||
// find left pin info. If doesn't exist - error.
|
||||
byte leftType = partGateClass.getPinType(leftName);
|
||||
if (leftType == UNKNOWN_PIN_TYPE)
|
||||
input.HDLError(leftName + " is not a pin in " + partName);
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L310
|
||||
if ((rightType == UNKNOWN_PIN_TYPE || rightType == INTERNAL_PIN_TYPE) &&
|
||||
!fullRightName.equals(rightName))
|
||||
input.HDLError(fullRightName + ": sub bus of an internal node may not be used");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L333
|
||||
if (selfFittingWidth) {
|
||||
if(!rightName.equals(fullRightName))
|
||||
input.HDLError(rightName + " may not be subscripted");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L346
|
||||
// check that right & left has the same width
|
||||
if (leftWidth != rightWidth)
|
||||
input.HDLError(leftName + "(" + leftWidth + ") and " + rightName + "(" + rightWidth +
|
||||
") have different bus widths");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L352
|
||||
// make sure that an internal pin is only fed once by a part's output pin
|
||||
if ((rightType == INTERNAL_PIN_TYPE) && (leftType == OUTPUT_PIN_TYPE)) {
|
||||
if (rightPinInfo.isInitialized(rightSubBus))
|
||||
input.HDLError("An internal pin may only be fed once by a part's output pin");
|
||||
|
||||
https://github.com/DavidSouther/nand2tetris/blob/8adbbd3d23c1a1bc746946891bd6d489da08594a/nand2tetris/org/nand2tetris/hack/simulators/gates/CompositeGateClass.java#L377
|
||||
// find connection type
|
||||
switch (leftType) {
|
||||
case INPUT_PIN_TYPE:
|
||||
switch (rightType) {
|
||||
case OUTPUT_PIN_TYPE:
|
||||
input.HDLError("Can't connect gate's output pin to part");
|
||||
|
||||
case OUTPUT_PIN_TYPE:
|
||||
switch (rightType) {
|
||||
case INPUT_PIN_TYPE:
|
||||
input.HDLError("Can't connect part's output pin to gate's input pin");
|
||||
Reference in New Issue
Block a user