Change directions to a stack-based, minecraft-focused machine

This commit is contained in:
Jeff 2024-04-03 00:22:01 -04:00
parent ea11d66887
commit 55bc02e00a
18 changed files with 6388 additions and 63 deletions

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
}

65
bit.test.ts Normal file
View File

@ -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
View File

@ -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
}
}

22
counter-machine.ts Normal file
View File

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

198
jest.config.js Normal file
View File

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

241
machine.test.ts Normal file
View File

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

73
machine.ts Normal file
View File

@ -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
}
}

51
machine.txt Normal file
View File

@ -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())
}

24
memory.test.ts Normal file
View File

@ -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)
})
})

View File

@ -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
}
}

199
minecraft-machine.test.ts Normal file
View File

@ -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())
}

65
minecraft-machine.ts Normal file
View File

@ -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 }
}

5067
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

78
ram.test.ts Normal file
View File

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

67
ram.ts Normal file
View File

@ -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"
}

126
word.test.ts Normal file
View File

@ -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
View File

@ -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)
}
}