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 {
|
||||
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 {
|
||||
|
|
@ -12,11 +13,16 @@ export default function bit(value: BinaryDigit): Bit {
|
|||
const ones = result % 2 as BinaryDigit
|
||||
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 {
|
||||
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 { emptyWord, wordFromBits } from "./word";
|
||||
import { Word, emptyWord } from "./word"
|
||||
|
||||
function memory(initialValue: Bit[])
|
||||
function memory(width: number)
|
||||
function memory(valueOrWidth: number | Bit[]) {
|
||||
let word = typeof valueOrWidth === 'number' ? emptyWord(valueOrWidth) : wordFromBits(valueOrWidth)
|
||||
export interface Memory<Width extends number> {
|
||||
value: () => Word<Width>
|
||||
set: (word: Word<Width>) => void
|
||||
}
|
||||
|
||||
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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
|
@ -13,7 +13,14 @@
|
|||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@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 { 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> {
|
||||
|
||||
plus: <OtherWidth extends number, ResultWidth = >(other: Word<OtherWidth>) => Word<GreaterOf<Width, OtherWidth>>
|
||||
bits: Bit[]
|
||||
length: Width
|
||||
concat<OtherWidth extends number>(other: Word<OtherWidth>): Word<Add<Width, OtherWidth>>
|
||||
toNumber(): number
|
||||
toString(): string
|
||||
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){
|
||||
// return value > other ? value as Value : other as Other
|
||||
// }
|
||||
type FixedLengthArray<T, Length extends number> = T[] & { length: Length }
|
||||
|
||||
type LessThan = number
|
||||
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>) {
|
||||
export function wordFromBits<const Width extends number, const BitArray extends FixedLengthArray<Bit, Width>>(bits: BitArray): Word<BitArray['length']> {
|
||||
|
||||
function plus<OtherWidth extends GreaterThan<Width>>(other: Word<OtherWidth>): Word<OtherWidth>
|
||||
function plus<OtherWidth extends LessOrEqual<Width>>(other: Word<OtherWidth>): Word<Width> {
|
||||
const newBits = addBits(bits, other.bits)
|
||||
function plus<OtherWidth extends number>(other: Word<OtherWidth>) {
|
||||
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) {
|
||||
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 {
|
||||
plus,
|
||||
subword,
|
||||
concat,
|
||||
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[]) {
|
||||
const bigEndianBits = [...bits].reverse()
|
||||
const bigEndianOtherBits = [...otherBits].reverse()
|
||||
const resultLength = Math.max(bits.length, otherBits.length)
|
||||
const paddedBits = zeroPad(bits, resultLength)
|
||||
const paddedOtherBits = zeroPad(otherBits, resultLength)
|
||||
|
||||
const pairedBits = zip(bigEndianBits, bigEndianOtherBits).reverse()
|
||||
const denulledBits = pairedBits.map(([a, b]) => [a ?? bit(0), b ?? bit(0)] as const)
|
||||
const pairedBits = zip(paddedBits, paddedOtherBits) as [Bit, Bit][]
|
||||
|
||||
const sumResults = denulledBits.reduce<SumResult>(({bits, carry}, [a, b]) => {
|
||||
const sumResults = pairedBits.reduceRight<SumResult>(({bits, carry}, [a, b]) => {
|
||||
const aPlusB = a.add(b)
|
||||
const aPlusBPlusCarry = aPlusB.ones.add(carry)
|
||||
const thisCarry = aPlusB.carry.add(aPlusBPlusCarry.carry).ones
|
||||
|
||||
bits.push(aPlusBPlusCarry.ones)
|
||||
|
||||
bits.splice(0, 0, aPlusBPlusCarry.ones)
|
||||
|
||||
return sumResult(bits, thisCarry)
|
||||
}, sumResult([], bit(0)))
|
||||
|
|
@ -79,6 +93,11 @@ function addBits(bits: Bit[], otherBits: Bit[]) {
|
|||
return sumResults
|
||||
}
|
||||
|
||||
function zeroPad(array: Bit[], length: number) {
|
||||
return [...Array(length - array.length).fill(bit(0)), ...array] as Bit[]
|
||||
}
|
||||
|
||||
|
||||
interface SumResult {
|
||||
bits: Bit[]
|
||||
carry: Bit
|
||||
|
|
@ -86,12 +105,12 @@ interface SumResult {
|
|||
|
||||
function sumResult(bits: Bit[], carry: Bit) {
|
||||
return {
|
||||
bits,
|
||||
carry}
|
||||
bits: bits,
|
||||
carry
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue