Change directions to a stack-based, minecraft-focused machine
This commit is contained in:
parent
ea11d66887
commit
55bc02e00a
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import bit from "./bit"
|
||||||
|
|
||||||
|
describe("bit", () => {
|
||||||
|
test("can be created from a 0", () => {
|
||||||
|
expect(bit(0).value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from a 1", () => {
|
||||||
|
expect(bit(1).value).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is equal to other bit when both values are 1", () => {
|
||||||
|
expect(bit(1).equals(bit(1))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is equal to other bit when both values are 0", () => {
|
||||||
|
expect(bit(0).equals(bit(0))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is not equal to other bit when value is 0 and other value is 1", () => {
|
||||||
|
expect(bit(0).equals(bit(1))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is not equal to other bit when value is 1 and other value is 0", () => {
|
||||||
|
expect(bit(1).equals(bit(0))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("addition", () => {
|
||||||
|
describe("sum", () => {
|
||||||
|
test("is 0 given 0 and 0", () => {
|
||||||
|
expect(bit(0).add(bit(0)).ones.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 1 given 1 and 0", () => {
|
||||||
|
expect(bit(1).add(bit(0)).ones.value).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 1 given 0 and 1", () => {
|
||||||
|
expect(bit(0).add(bit(1)).ones.value).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 0 given 1 and 1", () => {
|
||||||
|
expect(bit(1).add(bit(1)).ones.value).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("carry", () => {
|
||||||
|
test("is 0 given 0 and 0", () => {
|
||||||
|
expect(bit(0).add(bit(0)).carry.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 0 given 1 and 0", () => {
|
||||||
|
expect(bit(1).add(bit(0)).carry.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 0 given 0 and 1", () => {
|
||||||
|
expect(bit(0).add(bit(1)).carry.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is 1 given 1 and 1", () => {
|
||||||
|
expect(bit(1).add(bit(1)).carry.value).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
12
bit.ts
12
bit.ts
|
|
@ -2,7 +2,8 @@ type BinaryDigit = 0 | 1
|
||||||
|
|
||||||
export interface Bit {
|
export interface Bit {
|
||||||
value: BinaryDigit
|
value: BinaryDigit
|
||||||
add: (other: Bit) => { ones: Bit, carry: Bit }
|
add: (other: Bit) => { ones: Bit, carry: Bit },
|
||||||
|
equals: (other: Bit) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function bit(value: BinaryDigit): Bit {
|
export default function bit(value: BinaryDigit): Bit {
|
||||||
|
|
@ -12,11 +13,16 @@ export default function bit(value: BinaryDigit): Bit {
|
||||||
const ones = result % 2 as BinaryDigit
|
const ones = result % 2 as BinaryDigit
|
||||||
const carry = result === 2 ? 1 : 0
|
const carry = result === 2 ? 1 : 0
|
||||||
|
|
||||||
return { bit: bit(ones), carry: bit(carry) }
|
return { ones: bit(ones), carry: bit(carry) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(other: Bit) {
|
||||||
|
return value === other.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
add,
|
add,
|
||||||
value
|
value,
|
||||||
|
equals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { machine } from './machine'
|
||||||
|
import { ram } from './ram'
|
||||||
|
import { wordFromString } from './word'
|
||||||
|
|
||||||
|
export default () => machine(ram([
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("0011"),
|
||||||
|
wordFromString("1100"),
|
||||||
|
wordFromString("1111"),
|
||||||
|
wordFromString("1011"),
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0001"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0001"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("0000")
|
||||||
|
]))
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
/**
|
||||||
|
* For a detailed explanation regarding each configuration property, visit:
|
||||||
|
* https://jestjs.io/docs/configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
const config = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
// bail: 0,
|
||||||
|
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/tmp/jest_rs",
|
||||||
|
|
||||||
|
// Automatically clear mock calls, instances, contexts and results before every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
// collectCoverage: false,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
// collectCoverageFrom: undefined,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
// coverageDirectory: undefined,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Indicates which provider should be used to instrument code for coverage
|
||||||
|
// coverageProvider: "babel",
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
// coverageReporters: [
|
||||||
|
// "json",
|
||||||
|
// "text",
|
||||||
|
// "lcov",
|
||||||
|
// "clover"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: undefined,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: undefined,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// The default configuration for fake timers
|
||||||
|
// fakeTimers: {
|
||||||
|
// "enableGlobally": false
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files using an array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: undefined,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: undefined,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||||
|
// maxWorkers: "50%",
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "mjs",
|
||||||
|
// "cjs",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "json",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
// preset: undefined,
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: undefined,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state before every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: undefined,
|
||||||
|
|
||||||
|
// Automatically restore mock state and implementation before every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: undefined,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||||
|
// slowTestThreshold: 5,
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
// testEnvironment: "jest-environment-node",
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
// testMatch: [
|
||||||
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
|
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: undefined,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jest-circus/runner",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: undefined,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/",
|
||||||
|
// "\\.pnp\\.[^\\/]+$"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: undefined,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
import { machine } from "./machine"
|
||||||
|
import { ram } from "./ram"
|
||||||
|
import { wordFromNumber, wordFromString } from "./word"
|
||||||
|
|
||||||
|
describe("Two bit machine", () => {
|
||||||
|
let subject: ReturnType<typeof machine>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const machineRam = ram([
|
||||||
|
wordFromString("1011"),
|
||||||
|
wordFromString("0001"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("1110")
|
||||||
|
])
|
||||||
|
|
||||||
|
subject = machine(machineRam)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the program counter pointing at the second adddress in ram", () => {
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("01")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the instruction counter pointing at the first two bits in the current instruction", () => {
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("0")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the address register pointing to the first word stored in ram", () => {
|
||||||
|
expect(subject.addressRegister.value().toString()).toEqual("11")
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("for each clock cycle", () => {
|
||||||
|
|
||||||
|
describe("the instruction counter", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("increments by one", () => {
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resets to zero when already pointing at the end of the current word", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("0")
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test("increments the program counter when the instruction counter rolls over", () => {
|
||||||
|
subject.tick()
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("10")
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("if the current instruction is 00 (add)", () => {
|
||||||
|
test("ram[addressRegister] + ram[addressRegister + 1] is stored in ram[addressRegister + 2]", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("1111")
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.ram.read(wordFromNumber(4, 3)).toString()).toEqual("1011")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the instruction counter is incremented as usual", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("1111")
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the program counter is incremented as usual", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("1111")
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("001")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("if the current instruction is 01 (load)", () => {
|
||||||
|
test("ram[addressRegister] is loaded into addressRegister",() => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0011"),
|
||||||
|
wordFromString("0100"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("1111")
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.addressRegister.value().toString()).toEqual("0101")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ram is unchanged", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0100"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.ram.values().map(word => word.toString())).toEqual([
|
||||||
|
"0010",
|
||||||
|
"0100",
|
||||||
|
"0110"
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the instruction counter is incremented as usual", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0100"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the program counter is incremented as usual", () => {
|
||||||
|
const subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0100"),
|
||||||
|
wordFromString("0110"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("01")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("if the current instruction is 10 (jump)", () => {
|
||||||
|
describe("and ram[addressRegister] is nonzero", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("1111"),
|
||||||
|
wordFromString("1111"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the instruction counter is set to zero", () => {
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("0")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the program counter is set to ram[addressRegister + 1]", () => {
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("111")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("and ram[addressRegister] is zero", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("0010"),
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the instruction counter is incremented as usual", () => {
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the program counter is incremented as usual", () => {
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("001")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("if the current instruction is 11 (increment addressRegister)", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = machine(ram([
|
||||||
|
wordFromString("0010"),
|
||||||
|
wordFromString("1100"),
|
||||||
|
wordFromString("1111"),
|
||||||
|
wordFromString("1111")
|
||||||
|
]))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the address register is incremented", () => {
|
||||||
|
expect(subject.addressRegister.value().toString()).toEqual("11")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the instruction counter is incremented as usual", () => {
|
||||||
|
expect(subject.instructionCounter().value().toString()).toEqual("1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the program counter is incremented as usual", () => {
|
||||||
|
expect(subject.programCounter().value().toString()).toEqual("01")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import bit from "./bit";
|
||||||
|
import { memory } from "./memory";
|
||||||
|
import { Ram } from "./ram";
|
||||||
|
import { Word, emptyWord, wordFromNumber, wordFromString } from "./word";
|
||||||
|
|
||||||
|
export function machine<Width extends number>(ram: Ram<Width>) {
|
||||||
|
|
||||||
|
const programCounterRegister = memory(wordFromNumber(1, ram.addressLength))
|
||||||
|
|
||||||
|
const startAddress = ram.read(emptyWord(ram.addressLength)).subword(ram.wordSize - ram.addressLength, ram.addressLength)
|
||||||
|
|
||||||
|
const addressRegister = memory(startAddress)
|
||||||
|
|
||||||
|
const instructionCounterSize = Math.ceil(Math.log2(Math.floor(ram.wordSize / 2)))
|
||||||
|
const instructionCounterRegister = memory(emptyWord(instructionCounterSize))
|
||||||
|
|
||||||
|
function programCounter() {
|
||||||
|
return programCounterRegister
|
||||||
|
}
|
||||||
|
|
||||||
|
function instructionCounter() {
|
||||||
|
return instructionCounterRegister
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
|
||||||
|
const currentInstruction = ram.read(programCounterRegister.value()).subword(instructionCounterRegister.value().toNumber() * 2, 2)
|
||||||
|
if (currentInstruction.equals(wordFromString("00"))) {
|
||||||
|
const firstOperand = ram.read(addressRegister.value())
|
||||||
|
const secondOperand = ram.read(addressRegister.value().plus(wordFromNumber(1)).sum)
|
||||||
|
const result = firstOperand.plus(secondOperand).sum as Word<number>
|
||||||
|
|
||||||
|
ram.write(result as Word<Width>, addressRegister.value().plus(wordFromString("10")).sum)
|
||||||
|
incrementInstruction()
|
||||||
|
} else if (currentInstruction.equals(wordFromString("01"))) {
|
||||||
|
addressRegister.set(ram.read(addressRegister.value()))
|
||||||
|
incrementInstruction()
|
||||||
|
} else if (currentInstruction.equals(wordFromString("10"))) {
|
||||||
|
const current = ram.read(addressRegister.value())
|
||||||
|
const jumpAddress = ram.read(addressRegister.value().plus(wordFromNumber(1)).sum).subword(ram.wordSize - ram.addressLength, ram.addressLength)
|
||||||
|
|
||||||
|
if (current.toNumber() !== 0) {
|
||||||
|
programCounterRegister.set(jumpAddress)
|
||||||
|
instructionCounterRegister.set(emptyWord(instructionCounterSize))
|
||||||
|
} else {
|
||||||
|
incrementInstruction()
|
||||||
|
}
|
||||||
|
} else if (currentInstruction.equals(wordFromString("11"))) {
|
||||||
|
const nextValue = addressRegister.value().plus(wordFromNumber(1)).sum
|
||||||
|
addressRegister.set(nextValue)
|
||||||
|
|
||||||
|
incrementInstruction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementInstruction() {
|
||||||
|
const nextInstructionIndex = instructionCounterRegister.value().plus(wordFromNumber(1, instructionCounterSize))
|
||||||
|
instructionCounterRegister.set(nextInstructionIndex.sum)
|
||||||
|
|
||||||
|
if (nextInstructionIndex.carry.equals(bit(1))) {
|
||||||
|
const nextProgramCounterAddress = programCounterRegister.value().plus(wordFromNumber(1, ram.addressLength))
|
||||||
|
programCounterRegister.set(nextProgramCounterAddress.sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
programCounter,
|
||||||
|
addressRegister,
|
||||||
|
instructionCounter,
|
||||||
|
tick,
|
||||||
|
ram
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
00 Add ram[register] and ram[register + 1] and store in ram[register + 2]
|
||||||
|
01 Load ram[register] into register
|
||||||
|
10 Jump to ram[register + 1] if ram[register] > 0
|
||||||
|
11 Increment register
|
||||||
|
|
||||||
|
Using a 4-bit word size and 16 nibbles of ram:
|
||||||
|
|
||||||
|
|
||||||
|
Program:
|
||||||
|
|
||||||
|
Increment Count to 10, then stop
|
||||||
|
Clock speed is 1Hz
|
||||||
|
|
||||||
|
0x0 0x8
|
||||||
|
0x1 0q03
|
||||||
|
0x2 0q30
|
||||||
|
0x3 0q33
|
||||||
|
0x4 0q23
|
||||||
|
0x5 0q20
|
||||||
|
0x6 0x0
|
||||||
|
0x7 0x0
|
||||||
|
0x8 0x0
|
||||||
|
0x9 0x1
|
||||||
|
0xA 0x0
|
||||||
|
0xB 0x6
|
||||||
|
0xC 0x0
|
||||||
|
0xD 0x1
|
||||||
|
0xE 0x5
|
||||||
|
0xF 0x0
|
||||||
|
|
||||||
|
let counterMachine
|
||||||
|
|
||||||
|
counterMachine = require('./counter-machine').default()
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
counterMachine.tick()
|
||||||
|
|
||||||
|
logMachineState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function logMachineState() {
|
||||||
|
const programCounter = counterMachine.programCounter().value()
|
||||||
|
const instructionCounter = counterMachine.instructionCounter().value()
|
||||||
|
const nextInstruction = counterMachine.ram.read(programCounter).subword(instructionCounter.toNumber() * 2, 2)
|
||||||
|
|
||||||
|
console.log("Sum", counterMachine.ram.read(wordFromNumber(0xA)).toString())
|
||||||
|
console.log("ProgramCounter", programCounter.toString())
|
||||||
|
console.log("InstructionCounter", instructionCounter.toString())
|
||||||
|
console.log("Register", counterMachine.addressRegister.value().toString())
|
||||||
|
console.log("Next instructioq20", nextInstruction.toString())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { memory } from "./memory"
|
||||||
|
import { Word, wordFromNumber, wordFromString } from "./word"
|
||||||
|
|
||||||
|
describe("memory", () => {
|
||||||
|
test("can be created with an initial width", () => {
|
||||||
|
const subject = memory(5)
|
||||||
|
|
||||||
|
expect(subject.value().equals(wordFromString("00000"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from a word", () => {
|
||||||
|
const subject = memory(wordFromString("01010"))
|
||||||
|
|
||||||
|
expect(subject.value().equals(wordFromString("01010"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be updated with a new word", () => {
|
||||||
|
const subject = memory(4)
|
||||||
|
|
||||||
|
subject.set(wordFromString("1110") as Word<4>)
|
||||||
|
|
||||||
|
expect(subject.value().equals(wordFromString("1110"))).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
29
memory.ts
29
memory.ts
|
|
@ -1,12 +1,25 @@
|
||||||
import { Bit } from "./bit";
|
import { Word, emptyWord } from "./word"
|
||||||
import { emptyWord, wordFromBits } from "./word";
|
|
||||||
|
|
||||||
function memory(initialValue: Bit[])
|
export interface Memory<Width extends number> {
|
||||||
function memory(width: number)
|
value: () => Word<Width>
|
||||||
function memory(valueOrWidth: number | Bit[]) {
|
set: (word: Word<Width>) => void
|
||||||
let word = typeof valueOrWidth === 'number' ? emptyWord(valueOrWidth) : wordFromBits(valueOrWidth)
|
}
|
||||||
|
|
||||||
function set(word: Word) {
|
export function memory<Width extends number>(initialValue: Word<Width>): Memory<Width>
|
||||||
|
export function memory<Width extends number>(width: Width): Memory<Width>
|
||||||
|
export function memory<Width extends number>(valueOrWidth: Width | Word<Width>): Memory<Width> {
|
||||||
|
let word = typeof valueOrWidth === 'number' ? emptyWord(valueOrWidth) : valueOrWidth
|
||||||
|
|
||||||
|
function set(newWord: Word<Width>) {
|
||||||
|
word = newWord
|
||||||
|
}
|
||||||
|
|
||||||
|
function value() {
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { minecraftMachine } from "./minecraft-machine"
|
||||||
|
import { ram } from "./ram"
|
||||||
|
import { Word, wordFromNumber, wordFromString } from "./word"
|
||||||
|
|
||||||
|
const push = wordFromString("00") as Word<2>
|
||||||
|
const jump = wordFromString("01") as Word<2>
|
||||||
|
const add = wordFromString("10") as Word<2>
|
||||||
|
const write = wordFromString("11") as Word<2>
|
||||||
|
const any = wordFromString("00") as Word<2>
|
||||||
|
|
||||||
|
describe("Minecraft machine", () => {
|
||||||
|
let subject: ReturnType<typeof minecraftMachine>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const bank1Ram = toWordArray([
|
||||||
|
"1011",
|
||||||
|
"0001",
|
||||||
|
"0000",
|
||||||
|
"1110"
|
||||||
|
])
|
||||||
|
|
||||||
|
const bank2Ram = toWordArray([
|
||||||
|
"1111",
|
||||||
|
"0101",
|
||||||
|
"1010",
|
||||||
|
"1000",
|
||||||
|
"0011"
|
||||||
|
])
|
||||||
|
|
||||||
|
const instructions = [
|
||||||
|
"0000"
|
||||||
|
].map(value => wordFromString(value) as Word<4>)
|
||||||
|
|
||||||
|
subject = minecraftMachine(bank1Ram, bank2Ram, instructions)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the program counter pointing at 0x0", () => {
|
||||||
|
expect(subject.programCounter.value().toString()).toEqual("0000")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the address register pointing at 0x2", () => {
|
||||||
|
expect(subject.addressRegister.value().toString()).toEqual("0010")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with the extended address register pointing at the first memory bank", () => {
|
||||||
|
expect(subject.extendedAddressRegister.value().toString()).toEqual("0000")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("starts with an empty stack", () => {
|
||||||
|
expect(subject.stack.values().map(word => word.toString())).toEqual(["0000", "0000", "0000", "0000"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads the first ram bank with the provided values starting after reserved addresses, leaving unspecified values as zeros", () => {
|
||||||
|
expect(wordsToStrings(subject.bank1Ram.values())).toEqual([
|
||||||
|
"0010", "0000", "1011", "0001",
|
||||||
|
"0000", "1110", "0000", "0000",
|
||||||
|
"0000", "0000", "0000", "0000",
|
||||||
|
"0000", "0000", "0000", "0000"
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads the second ram bank with the provided values starting after reserved addresses, leaving unspecified values as zeros", () => {
|
||||||
|
expect(wordsToStrings(subject.bank2Ram.values())).toEqual([
|
||||||
|
"0010", "0000", "0000", "0000",
|
||||||
|
"0000", "1111", "0101", "1010",
|
||||||
|
"1000", "0011", "0000", "0000",
|
||||||
|
"0000", "0000", "0000", "0000"
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("push instruction", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = minecraftMachine(toWordArray(["1101", "0110"]), toWordArray(["1010"]), [push.concat(push)])
|
||||||
|
subject.addressRegister.set(wordFromString("0010") as Word<4>)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads value at address register onto the stack", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(wordsToStrings(subject.stack.values())).toEqual(["1101", "0000", "0000", "0000"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("increments the stack pointer", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.stackPointer.value().toString()).toEqual("01")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads the value on top of the existing stack", () => {
|
||||||
|
subject.tick()
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(wordsToStrings(subject.stack.values())).toEqual(["1101", "0110", "0000", "0000"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("increments the address register", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.addressRegister.value().toString()).toEqual("0011")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads value from bank 2 if extended address register is pointing there", () => {
|
||||||
|
subject.extendedAddressRegister.set(wordFromNumber(1, 4) as Word<4>)
|
||||||
|
subject.addressRegister.set(wordFromString("0101") as Word<4>)
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(wordsToStrings(subject.stack.values())).toEqual(["1010", "0000", "0000", "0000"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not increment the stack pointer if the stack is full", () => {
|
||||||
|
subject.stackPointer.set(wordFromString("11"))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.stackPointer.value().toString()).toEqual("11")
|
||||||
|
})
|
||||||
|
|
||||||
|
testIncrementsTheProgramCounter(() => subject)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("jump instruction", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = minecraftMachine(toWordArray([]), toWordArray([]), [jump.concat(any)])
|
||||||
|
subject.stackPointer.set(wordFromString("01"))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("when the previous stack value is greater than zero", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject.stack.write(wordFromString("0001") as Word<4>, wordFromString("00"))
|
||||||
|
subject.stack.write(wordFromString("0100") as Word<4>, wordFromString("01"))
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets the program counter to the current stack value", () => {
|
||||||
|
expect(subject.programCounter.value().toString()).toEqual("0100")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resets the instruction pointer", () => {
|
||||||
|
expect(subject.instructionPointer.value().toString()).toEqual("0")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("pops the current value", () => {
|
||||||
|
expect(subject.stackPointer.value().toString()).toEqual("00")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe("when the previous stack value is zero", () => {
|
||||||
|
test("pops the current value", () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
testIncrementsTheProgramCounter(() => subject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function testIncrementsTheProgramCounter(getSubject: () => ReturnType<typeof minecraftMachine>) {
|
||||||
|
|
||||||
|
let subject: ReturnType<typeof minecraftMachine>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subject = getSubject()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not increment the program counter if the current instruction is the first", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.programCounter.value().toString()).toEqual("0000")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("increments the program counter if the current instruction is the second", () => {
|
||||||
|
subject.tick()
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.programCounter.value().toString()).toEqual("0001")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toggles the instruction pointer", () => {
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.instructionPointer.value().toString()).toEqual("1")
|
||||||
|
|
||||||
|
subject.tick()
|
||||||
|
|
||||||
|
expect(subject.instructionPointer.value().toString()).toEqual("0")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWordArray(values: string[]) {
|
||||||
|
return values.map(wordFromString) as Word<4>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordsToStrings(values: Word<number>[]) {
|
||||||
|
return values.map(value => value.toString())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import bit from "./bit";
|
||||||
|
import { memory } from "./memory";
|
||||||
|
import { Ram, ram } from "./ram";
|
||||||
|
import { Word, emptyWord, wordFromNumber, wordFromString } from "./word";
|
||||||
|
|
||||||
|
const bank1GeneralAddressesStart = wordFromString("0010") as Word<4>
|
||||||
|
const bank2GeneralAddressesStart = wordFromString("0101") as Word<4>
|
||||||
|
|
||||||
|
const addressRegisterAddress = wordFromString("0000") as Word<4>
|
||||||
|
const extendedAddressRegisterAddress = wordFromString("0001") as Word<4>
|
||||||
|
|
||||||
|
const programCounterAddress = wordFromString("0010") as Word<4>
|
||||||
|
const overflowFlagAddress = wordFromString("0011") as Word<4>
|
||||||
|
const inputPinAddress = wordFromString("0100") as Word<4>
|
||||||
|
|
||||||
|
|
||||||
|
export function minecraftMachine(initialBank1Values: Word<4>[], initialBank2Values: Word<4>[], instructions: Word<4>[]) {
|
||||||
|
|
||||||
|
const bank1Ram = ram({ size: 16, width: 4 })
|
||||||
|
const bank2Ram = ram({ size: 16, width: 4 })
|
||||||
|
const stack = ram({ size: 4, width: 4 })
|
||||||
|
const stackPointer = memory(wordFromString("00"))
|
||||||
|
const instructionPointer = memory(wordFromString("0"))
|
||||||
|
|
||||||
|
initialBank1Values.forEach((value, index) => bank1Ram.write(value, wordFromNumber(index).plus(bank1GeneralAddressesStart).sum))
|
||||||
|
initialBank2Values.forEach((value, index) => bank2Ram.write(value, wordFromNumber(index).plus(bank2GeneralAddressesStart).sum))
|
||||||
|
|
||||||
|
const addressRegister = bank1Ram.get(addressRegisterAddress)
|
||||||
|
const extendedAddressRegister = bank1Ram.get(extendedAddressRegisterAddress)
|
||||||
|
addressRegister.set(wordFromNumber(2, 4) as Word<4>)
|
||||||
|
|
||||||
|
bank2Ram.write(addressRegister.value(), addressRegisterAddress)
|
||||||
|
bank2Ram.write(extendedAddressRegister.value(), extendedAddressRegisterAddress)
|
||||||
|
|
||||||
|
|
||||||
|
const programCounter = bank2Ram.get(programCounterAddress)
|
||||||
|
const overflowFlag = bank2Ram.get(overflowFlagAddress)
|
||||||
|
const inputPin = bank2Ram.get(inputPinAddress)
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
incrementInstruction()
|
||||||
|
stack.write(readCurrent(), stackPointer.value())
|
||||||
|
|
||||||
|
if (!stackPointer.value().equals(wordFromString("11"))) {
|
||||||
|
stackPointer.set(stackPointer.value().plus(wordFromString("01")).sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
addressRegister.set(addressRegister.value().plus(wordFromNumber(1, 4)).sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementInstruction() {
|
||||||
|
if (instructionPointer.value().equals(wordFromString("1"))) {
|
||||||
|
programCounter.set(programCounter.value().plus(wordFromNumber(1, 4)).sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
instructionPointer.set(instructionPointer.value().plus(wordFromString("1")).sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCurrent() {
|
||||||
|
const currentBank = extendedAddressRegister.value().toString() === "0000" ? bank1Ram : bank2Ram
|
||||||
|
return currentBank.read(addressRegister.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { programCounter, instructionPointer, addressRegister, extendedAddressRegister, stack, stackPointer, tick, bank1Ram, bank2Ram }
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
|
@ -4,7 +4,7 @@
|
||||||
"description": "Tiny computer",
|
"description": "Tiny computer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|
@ -13,7 +13,14 @@
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.24.0",
|
||||||
|
"@babel/preset-env": "^7.24.0",
|
||||||
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
"@rimbu/typical": "^0.8.0",
|
"@rimbu/typical": "^0.8.0",
|
||||||
"@types/node": "^20.11.16"
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.11.16",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"math-types": "^0.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { ram } from "./ram"
|
||||||
|
import { Word, emptyWord, wordFromString } from "./word"
|
||||||
|
|
||||||
|
describe("Ram", () => {
|
||||||
|
test("begins life with the specified number of empty words of the specified width", () =>{
|
||||||
|
const subject = ram({
|
||||||
|
width: 6,
|
||||||
|
size: 4
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(subject.values().every(word => word.equals(wordFromString("000000")))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from an array of strings", () => {
|
||||||
|
const subject = ram([
|
||||||
|
"0000",
|
||||||
|
"1111"
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(subject.values().map(word => word.toString())).toEqual([
|
||||||
|
"0000",
|
||||||
|
"1111"
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from an array of words", () => {
|
||||||
|
const subject = ram([
|
||||||
|
wordFromString("0000"),
|
||||||
|
wordFromString("1000"),
|
||||||
|
wordFromString("1011"),
|
||||||
|
wordFromString("0101"),
|
||||||
|
wordFromString("1111"),
|
||||||
|
wordFromString("1111")
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(subject.values().map(word => word.toString())).toEqual([
|
||||||
|
"0000",
|
||||||
|
"1000",
|
||||||
|
"1011",
|
||||||
|
"0101",
|
||||||
|
"1111",
|
||||||
|
"1111"
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reports its word size", () => {
|
||||||
|
const subject = ram([wordFromString("001")])
|
||||||
|
|
||||||
|
expect(subject.wordSize).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reports its address length", () => {
|
||||||
|
expect(ram({ size: 48, width: 8 }).addressLength).toEqual(6)
|
||||||
|
expect(ram({ size: 4, width: 8 }).addressLength).toEqual(2)
|
||||||
|
expect(ram({ size: 5, width: 8 }).addressLength).toEqual(3)
|
||||||
|
expect(ram({ size: 8, width: 8 }).addressLength).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns the memory value for a given index", () => {
|
||||||
|
const subject = ram({
|
||||||
|
width: 4,
|
||||||
|
size: 4
|
||||||
|
})
|
||||||
|
|
||||||
|
subject.write(wordFromString("1010") as Word<4>, wordFromString("10"))
|
||||||
|
|
||||||
|
expect(subject.read(wordFromString("10")).equals(wordFromString("1010"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns the memory for a given index", () => {
|
||||||
|
const subject = ram([
|
||||||
|
"0000",
|
||||||
|
"0001"
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(subject.get(wordFromString("1")).value().toString()).toEqual("0001")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Memory, memory } from "./memory"
|
||||||
|
import { Word, wordFromString } from "./word"
|
||||||
|
|
||||||
|
export interface Ram<Width extends number> {
|
||||||
|
get(address: Word<number>): Memory<Width>
|
||||||
|
addressLength: number
|
||||||
|
values: () => Word<Width>[]
|
||||||
|
write: (value: Word<Width>, address: Word<number>) => void
|
||||||
|
read: (address: Word<number>) => Word<Width>
|
||||||
|
wordSize: Width
|
||||||
|
}
|
||||||
|
|
||||||
|
type RamDimensions<Width extends number> = { size: number, width: Width }
|
||||||
|
|
||||||
|
export function ram<Width extends number>(initialValues: string[]): Ram<Width>
|
||||||
|
export function ram<Width extends number>(initialValues: Word<Width>[]): Ram<Width>
|
||||||
|
export function ram<Width extends number>({ size, width }: RamDimensions<Width>): Ram<Width>
|
||||||
|
export function ram<Width extends number>(arg: RamDimensions<Width> | (Word<Width>[]) | string[]): Ram<Width> {
|
||||||
|
|
||||||
|
const memorySpace = unpackArgToMemoryArray(arg)
|
||||||
|
|
||||||
|
const wordSize = memorySpace[0].value().bits.length
|
||||||
|
const addressLength = Math.ceil(Math.log2(memorySpace.length))
|
||||||
|
|
||||||
|
function write(value: Word<Width>, address: Word<number>) {
|
||||||
|
memorySpace[address.toNumber()].set(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(address: Word<number>) {
|
||||||
|
return get(address).value()
|
||||||
|
}
|
||||||
|
|
||||||
|
function values() {
|
||||||
|
return memorySpace.map(memory => memory.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(address: Word<number>) {
|
||||||
|
return memorySpace[address.toNumber()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
write,
|
||||||
|
read,
|
||||||
|
get,
|
||||||
|
wordSize,
|
||||||
|
addressLength,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpackArgToMemoryArray<Width extends number>(arg: RamDimensions<Width> | (Word<Width>[]) | string[]) {
|
||||||
|
return isDimensionsArg(arg) ? memoryFromSizeAndWidth(arg.size, arg.width) : memoryFromArrayArg(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
function memoryFromArrayArg<Width extends number>(array: Word<Width>[] | string[]) {
|
||||||
|
const isStringArray = typeof array[0] === "string"
|
||||||
|
const words = isStringArray ? (array as string[]).map(stringValue => wordFromString(stringValue) as Word<Width>) : array as Word<Width>[]
|
||||||
|
return words.map(word => memory<Width>(word)) as Memory<Width>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function memoryFromSizeAndWidth<Width extends number>(size: number, width: Width) {
|
||||||
|
return Array(size).fill(() => 0).map(() => memory(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDimensionsArg<Width extends number>(arg: any): arg is RamDimensions<Width> {
|
||||||
|
return typeof arg === "object" && typeof arg.size === "number" && typeof arg.width === "number"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import bit from "./bit"
|
||||||
|
import { emptyWord, wordFromBits, wordFromNumber, wordFromString } from "./word"
|
||||||
|
|
||||||
|
describe("word", () => {
|
||||||
|
test("can be created with an array of bits", () => {
|
||||||
|
const sourceBits = [bit(0), bit(1), bit(1)]
|
||||||
|
|
||||||
|
const subject = wordFromBits(sourceBits)
|
||||||
|
|
||||||
|
expect(subject.bits).toEqual(sourceBits)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from a binary literal", () => {
|
||||||
|
const subject = wordFromNumber(0b10100111)
|
||||||
|
|
||||||
|
expect(subject.bits.map(bit => bit.value)).toEqual([1, 0, 1, 0, 0, 1, 1, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from a binary literal with length", () => {
|
||||||
|
const subject = wordFromNumber(0b1, 3)
|
||||||
|
|
||||||
|
expect(subject.bits.map(bit => bit.value)).toEqual([0, 0, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from string", () => {
|
||||||
|
const subject = wordFromString("01100")
|
||||||
|
|
||||||
|
expect(subject.bits.map(bit => bit.value)).toEqual([0, 1, 1, 0, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be created from length", () => {
|
||||||
|
const subject = emptyWord(5)
|
||||||
|
|
||||||
|
expect(subject.equals(wordFromString("00000"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be converted to number", () => {
|
||||||
|
const subject = wordFromString("11001")
|
||||||
|
|
||||||
|
expect(subject.toNumber()).toEqual(25)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("can be converted to string", () => {
|
||||||
|
const subject = wordFromString("01110001111")
|
||||||
|
|
||||||
|
expect(subject.toString()).toBe("01110001111")
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("subword", () => {
|
||||||
|
test("returns the bits from the start position up to but not including start plus length", () => {
|
||||||
|
const result = wordFromString("1001101000101000010111111").subword(3, 6)
|
||||||
|
|
||||||
|
expect(result.equals(wordFromString("110100"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns the word starting from the start position when length is omitted", () => {
|
||||||
|
const result = wordFromString("1001101").subword(2)
|
||||||
|
|
||||||
|
expect(result.equals(wordFromString("01101"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns a truncated word when the requested length overflows the word width", () => {
|
||||||
|
const result = wordFromString("11000011110").subword(7, 9999)
|
||||||
|
|
||||||
|
expect(result.equals(wordFromString("1110")))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns an empty word when the start position is past the end of the word", () => {
|
||||||
|
const result = wordFromString("11000011110").subword(9999)
|
||||||
|
|
||||||
|
expect(result.equals(wordFromBits([])))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("concat", () => {
|
||||||
|
test("returns the combined bits from this word and the other word", () => {
|
||||||
|
const result = wordFromString("100").concat(wordFromString("110"))
|
||||||
|
|
||||||
|
expect(result.equals(wordFromString("100110"))).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("equals", () => {
|
||||||
|
test("is true when two words have the same bits", () => {
|
||||||
|
expect(wordFromString("01100").equals(wordFromString("01100"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is false when two words have different lengths", () => {
|
||||||
|
expect(wordFromString("01100").equals(wordFromString("011000"))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is false when two words have different bits", () => {
|
||||||
|
expect(wordFromString("0111").equals(wordFromString("1010"))).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("addition", () => {
|
||||||
|
test("returns a carry of 0 when there is no overflow", () => {
|
||||||
|
const a = wordFromBits([bit(0), bit(0), bit(1)])
|
||||||
|
const b = wordFromNumber(0b101)
|
||||||
|
|
||||||
|
expect(a.plus(b).carry.value).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns a carry of 1 when there is overflow", () => {
|
||||||
|
const a = wordFromBits([bit(0), bit(1), bit(1)])
|
||||||
|
const b = wordFromNumber(0b111)
|
||||||
|
|
||||||
|
expect(a.plus(b).carry.value).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns the sum", () => {
|
||||||
|
const a = wordFromBits([bit(0), bit(0), bit(1)])
|
||||||
|
const b = wordFromNumber(0b101)
|
||||||
|
|
||||||
|
expect(a.plus(b).sum.bits.map(bit => bit.value)).toEqual([1, 1, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns a word with a width equal to the greater of the two input widths", () => {
|
||||||
|
const a = wordFromNumber(0b10)
|
||||||
|
const b = wordFromNumber(0b101010111)
|
||||||
|
|
||||||
|
expect(a.plus(b).sum.bits).toHaveLength(9)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
117
word.ts
117
word.ts
|
|
@ -1,76 +1,90 @@
|
||||||
import bit, { Bit } from "./bit"
|
import bit, { Bit } from "./bit"
|
||||||
import { range, zip } from "lodash"
|
import { range, zip } from "lodash"
|
||||||
|
import { Add, Max } from "math-types"
|
||||||
|
|
||||||
type GreaterThan<Threshold extends number> = number
|
|
||||||
type LessOrEqual<Threshold extends number> = number
|
|
||||||
|
|
||||||
export interface Word<Width extends number> {
|
export interface Word<Width extends number> {
|
||||||
|
concat<OtherWidth extends number>(other: Word<OtherWidth>): Word<Add<Width, OtherWidth>>
|
||||||
plus: <OtherWidth extends number, ResultWidth = >(other: Word<OtherWidth>) => Word<GreaterOf<Width, OtherWidth>>
|
toNumber(): number
|
||||||
bits: Bit[]
|
toString(): string
|
||||||
length: Width
|
plus: <OtherWidth extends number>(other: Word<OtherWidth>) => { sum: Word<Max<Width, OtherWidth>>, carry: Bit }
|
||||||
|
bits: Bit[] & { length: Width }
|
||||||
|
subword: <Width extends number>(start: number, length?: Width) => Word<Width>
|
||||||
|
equals: (word?: Word<number>) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// function greaterOf<Value extends number, Other extends number>(value: Value, other: Other){
|
type FixedLengthArray<T, Length extends number> = T[] & { length: Length }
|
||||||
// return value > other ? value as Value : other as Other
|
|
||||||
// }
|
|
||||||
|
|
||||||
type LessThan = number
|
export function wordFromBits<const Width extends number, const BitArray extends FixedLengthArray<Bit, Width>>(bits: BitArray): Word<BitArray['length']> {
|
||||||
type GreaterOrEqual = number
|
|
||||||
|
|
||||||
function isLessThan(value: number, other: number): value is LessThan {
|
|
||||||
return value < other
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGreaterThanOrEqual(value: number, other: number): value is GreaterOrEqual {
|
|
||||||
return value >= other
|
|
||||||
}
|
|
||||||
|
|
||||||
function greaterOf<A extends LessThan, B>(value: LessThan, other: B): LessThan
|
|
||||||
function greaterOf<A extends GreaterOrEqual, B>(value: GreaterOrEqual, other: B): GreaterOrEqual
|
|
||||||
function greaterOf<A extends number, B extends number>(value: A, other: B): LessThan | GreaterOrEqual {
|
|
||||||
const result = Math.max(value, other)
|
|
||||||
return isLessThan(value, other) ? value as GreaterThan : other as LessThan
|
|
||||||
}
|
|
||||||
|
|
||||||
const duh = wordFromBits([bit(0)] as const).plus(wordFromBits([bit(0), bit(0)]))
|
|
||||||
|
|
||||||
type FixedLengthArray<T, Length extends number> = Readonly<Array<T> & { length: Length}>
|
|
||||||
|
|
||||||
export function wordFromBits<Width extends Readonly<number>>(bits: readonly FixedLengthArray<Bit, Width>) {
|
|
||||||
|
|
||||||
function plus<OtherWidth extends GreaterThan<Width>>(other: Word<OtherWidth>): Word<OtherWidth>
|
function plus<OtherWidth extends number>(other: Word<OtherWidth>) {
|
||||||
function plus<OtherWidth extends LessOrEqual<Width>>(other: Word<OtherWidth>): Word<Width> {
|
const newBits = addBits(bits, other.bits)
|
||||||
const newBits = addBits(bits, other.bits)
|
|
||||||
|
|
||||||
return wordFromBits(newBits.bits)
|
return {
|
||||||
|
sum: wordFromBits(newBits.bits as FixedLengthArray<Bit, Max<Width, OtherWidth>>) as Word<Max<Width, OtherWidth>>,
|
||||||
|
carry: newBits.carry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subword(start: number, length?: number) {
|
function subword(start: number, length?: number) {
|
||||||
return wordFromBits(bits.slice(start, length !== undefined ? start + length : undefined))
|
return wordFromBits(bits.slice(start, length !== undefined ? start + length : undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function concat<OtherWidth extends number>(other: Word<OtherWidth>) {
|
||||||
|
return wordFromBits([...bits, ...other.bits])
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(other?: Word<number>) {
|
||||||
|
return other !== undefined
|
||||||
|
&& other.bits.length === bits.length
|
||||||
|
&& other.bits.every((otherBit, index) => bits[index].equals(otherBit))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber() {
|
||||||
|
return Number.parseInt(bits.map(bit => bit.value).join(""), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString() {
|
||||||
|
return bits.map(bit => bit.value).join("")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plus,
|
plus,
|
||||||
subword,
|
subword,
|
||||||
|
concat,
|
||||||
bits,
|
bits,
|
||||||
length: bits.length
|
equals,
|
||||||
}
|
toNumber,
|
||||||
|
toString
|
||||||
|
} as Word<Width>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wordFromNumber(source: number, length?: number) {
|
||||||
|
const bitArray = source.toString(2).split("").map(char => bit(Number.parseInt(char) as 0 | 1))
|
||||||
|
const paddedBitArray = length === undefined ? bitArray : zeroPad(bitArray, length)
|
||||||
|
|
||||||
|
return wordFromBits(paddedBitArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wordFromString(source: string) {
|
||||||
|
const bitArray = source.split("").map(char => bit(Number.parseInt(char) as 0 | 1))
|
||||||
|
|
||||||
|
return wordFromBits(bitArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addBits(bits: Bit[], otherBits: Bit[]) {
|
function addBits(bits: Bit[], otherBits: Bit[]) {
|
||||||
const bigEndianBits = [...bits].reverse()
|
const resultLength = Math.max(bits.length, otherBits.length)
|
||||||
const bigEndianOtherBits = [...otherBits].reverse()
|
const paddedBits = zeroPad(bits, resultLength)
|
||||||
|
const paddedOtherBits = zeroPad(otherBits, resultLength)
|
||||||
|
|
||||||
const pairedBits = zip(bigEndianBits, bigEndianOtherBits).reverse()
|
const pairedBits = zip(paddedBits, paddedOtherBits) as [Bit, Bit][]
|
||||||
const denulledBits = pairedBits.map(([a, b]) => [a ?? bit(0), b ?? bit(0)] as const)
|
|
||||||
|
|
||||||
const sumResults = denulledBits.reduce<SumResult>(({bits, carry}, [a, b]) => {
|
const sumResults = pairedBits.reduceRight<SumResult>(({bits, carry}, [a, b]) => {
|
||||||
const aPlusB = a.add(b)
|
const aPlusB = a.add(b)
|
||||||
const aPlusBPlusCarry = aPlusB.ones.add(carry)
|
const aPlusBPlusCarry = aPlusB.ones.add(carry)
|
||||||
const thisCarry = aPlusB.carry.add(aPlusBPlusCarry.carry).ones
|
const thisCarry = aPlusB.carry.add(aPlusBPlusCarry.carry).ones
|
||||||
|
|
||||||
bits.push(aPlusBPlusCarry.ones)
|
bits.splice(0, 0, aPlusBPlusCarry.ones)
|
||||||
|
|
||||||
return sumResult(bits, thisCarry)
|
return sumResult(bits, thisCarry)
|
||||||
}, sumResult([], bit(0)))
|
}, sumResult([], bit(0)))
|
||||||
|
|
@ -79,6 +93,11 @@ function addBits(bits: Bit[], otherBits: Bit[]) {
|
||||||
return sumResults
|
return sumResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zeroPad(array: Bit[], length: number) {
|
||||||
|
return [...Array(length - array.length).fill(bit(0)), ...array] as Bit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface SumResult {
|
interface SumResult {
|
||||||
bits: Bit[]
|
bits: Bit[]
|
||||||
carry: Bit
|
carry: Bit
|
||||||
|
|
@ -86,12 +105,12 @@ interface SumResult {
|
||||||
|
|
||||||
function sumResult(bits: Bit[], carry: Bit) {
|
function sumResult(bits: Bit[], carry: Bit) {
|
||||||
return {
|
return {
|
||||||
bits,
|
bits: bits,
|
||||||
carry}
|
carry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyWord<Width extends number>(width: Width): Word<Width> {
|
export function emptyWord<Width extends number>(width: Width): Word<Width> {
|
||||||
const bits = range(0, width).map(() => bit(0))
|
const bits = range(0, width).map(() => bit(0)) as Bit[] & { length: Width }
|
||||||
return wordFromBits(bits)
|
return wordFromBits(bits)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue