summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile36
-rw-r--r--main.cpp169
-rw-r--r--main.hpp151
4 files changed, 358 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b379de5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.gdb_history
+ov
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..1d860b2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+DESTDIR=/
+SRCFILE=main.cpp
+O=0
+CFLAGS += -Wextra -Wall -g -O$O -I. -oov -finput-charset=UTF-8 -fextended-identifiers
+LIBS +=
+CC=c++
+VGARGS += --leak-check=full --track-origins=yes --verbose --log-file=valgrind-out.txt --suppressions=/usr/lib/i386-linux-gnu/valgrind/default.supp --show-leak-kinds=all --leak-resolution=high
+
+.NOTPARALLEL:
+default:
+ $(CC) $(CFLAGS) $(SRCFILE) $(LIBS)
+
+# tests if code compiles under gcc, clang and tcc
+cc:
+ make -e CC=tcc
+ make -e CC=gcc
+ make -e CC=clang
+
+install:
+ mkdir -p $(DESTDIR)/usr/bin/
+ cp ov $(DESTDIR)/usr/bin/
+
+distclean:
+ rm ov -f
+
+clean:
+ rm ov -f
+
+prepare:
+ apt install build-essential -y
+
+# developing is to be done on i386. for example the default suppression file is hardcoded for i386 here:
+# developing is to be done on bullseye.
+
+valgrind:
+ G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind $(VGARGS) ./ov
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..7254b1e
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,169 @@
+#include <iostream> /* src, dest, instr, padding */
+#include "main.hpp"
+using namespace std;
+using namespace ov;
+namespace ov {
+ template<typename value_type, typename index_type>
+ value_type Ram<value_type, index_type>::peek (index_type addr) {
+ if (addr < ov->pas)
+ return ov->pc & 1 << addr;
+ if (addr >= (unsigned int) 1 << ov->ras)
+ return ov->opts[addr-(1 << ov->ras)];
+ if (addr-ov->pas < IOLEN)
+ return ov->io[addr-ov->rs];
+ return storage[addr];
+ }
+ template<typename value_type, typename index_type>
+ void Ram<value_type, index_type>::poke (index_type addr, value_type val) {
+ if (addr < ov->pas) {
+ ov->pc &= ~(1 << addr);
+ if (val)
+ ov->pc |= 1 << addr;
+ } /* we fall through */
+ if (addr >= (unsigned int) 1 << ov->ras)
+ ov->opts[addr-ov->rs] = val;
+ if (addr >= ov->pas && addr-ov->pas < IOLEN) {
+ if (ov->opts[BUFIN] && addr-ov->pas == INA && val == 0) { /* machine read */
+ if (!ov->inbuf.empty()) { /* and wants more. oh, it looks like we */
+ ov->io[IN] = ov->inbuf.front(); /* have more. we pop the */
+ ov->inbuf.pop(); /* queue, set the input available bit and */
+ ov->io[INA] = 1; /* return, so actual set will not be */
+ return; /* applied, because input is ready. */
+ } /* if we have no input, we just store the zero */
+ } /* we do similar things for output. whenever the machine is ready to */
+ if (ov->opts[BUFOUT] && addr-ov->pas == OUTA && val == 1) { /* output, it */
+ ov->outbuf.push(ov->io[OUT]); /* sets OUTA bit. we push the OUT */
+ ov->io[OUT] = 0; /* value into the output queue and clear OUTA bit */
+ return; /* and therefore indicating the host has read the output. */
+ }
+ ov->io[addr-ov->pas] = val;
+ } /* we fall through, but only if !BUF__ */
+ storage[addr] = val;
+ }
+ template<typename value_type, typename index_type>
+ Ram<value_type, index_type>::Ram (Ov * ov) {
+ this->ov = ov;
+ storage.reserve(ov->rs+ov->pas);
+ for (unsigned int i = 0; i <= ov->rs+ov->pas; i++)
+ poke(i, 0);
+ }
+ template<typename value_type, typename index_type> // inventor did not say about
+ Program<value_type, index_type>::Program (Ov * ov) { // initializing program memory, so I
+ this->ov = ov; // do not do that. but by "spec", ram
+ storage.reserve(ov->ps); // is set to zero and code starts at
+ } // pc == 0, unless of course set with
+ struct NotImplemented : public exception { // Ov::pc.
+ const char * what () const throw () {
+ return "Not implemented.";
+ }
+ } NotImplemented;
+ struct NotAvailable : public exception {
+ const char * what () const throw () {
+ return "I/O is currently not available. You can try again.";
+ }
+ } NotAvailable;
+ struct BufferingRequired : public exception {
+ const char * what () const throw () {
+ return "I/O buffering must be enabled for this function, but it's not.";
+ }
+ } BufferingRequired;
+ struct NotAligned : public exception {
+ const char * what () const throw () {
+ return "Byte count can't be deserialized without remainder.";
+ }
+ } NotAligned;
+ void Ov::step () { // ko se izvaja inštrukcija, kaže števec programa na naslednjo.
+ bool b[pas]; // buffer. you have to first read all and then write for COPY
+ switch (pm(pc++).i) { // predstavljaj si, da so oklepaji okoli pc++ oglati (;
+ case COPY:
+ for (int i = 0; i < pas; i++)
+ b[i] = ram[pm(pc).s+i];
+ for (int i = 0; i < pas; i++)
+ ram[pm(pc).d+i] = b[i];
+ break;
+ case NAND:
+ ram[pm(pc).d] = !(ram[pm(pc).s] & ram[pm(pc).d]);
+ break;
+ }
+ }
+ bool Ov::out () { /* output from machine. throw NotAvailable when there's nothing */
+ if (opts[BUFOUT]) {
+ if (outbuf.empty())
+ throw NotAvailable;
+ bool o = outbuf.front();
+ outbuf.pop(); /* unconveniently, pop returns nothing */
+ return o;
+ }
+ if (!(io[OUTA]))
+ throw NotAvailable;
+ io[OUTA] = 0;
+ return io[OUT];
+ } /* if BUFOUT is set in opts, this is read from the buffer instead */
+ char Ov::outc () { /* output a byte from machine or throw NotAvailable if not enough bits */
+ if (!opts[BUFOUT])
+ throw BufferingRequired;
+ if (outbuf.size() < 8)
+ throw NotAvailable;
+ char r;
+ for (int i = 0; i < 8; i++) { // bitwise endianness is big
+ bool b = outbuf.front();
+ r &= ~(1 << (8-i));
+ if (b)
+ r |= 1 << (8-i);
+ outbuf.pop();
+ }
+ return r;
+ } /* buffering must be enabled for this to work. */
+ void Ov::in (bool i) { /* input to machine. thrw NotAvailable when program hasn't read */
+ if (opts[BUFOUT]) { /* for BUFOUT input we ONLY insert into the queue if machine */
+ if (io[INA]) /* "EWOULDBLOCK", in case it can read we put directly to io. */
+ inbuf.push(i);
+ io[IN] = i;
+ io[INA] = 1;
+ return;
+ }
+ if (io[INA]) /* bit is set, so program in VM must first clear the bit. */
+ throw NotAvailable;
+ io[IN] = i;
+ io[INA] = 1;
+ } /* if BUFIN is set in opts, this is put to the buffer instead */
+ void Ov::inc (char i) { // input a byte to the machine
+ if (!opts[BUFIN])
+ throw BufferingRequired;
+ if (inbuf.size() < 8)
+ throw NotAvailable;
+ for (int x = 7; x >= 0; x--) {
+ in(i & 1 << x);
+ }
+ } // buffering must be enabled for this to work.
+ struct instr Ov::deserialize (const char * c) { // treats i as array of is size
+ struct instr r;
+ for (int i = 0; i < ras; i++)
+ if (c[i/8] & 1 << (i%8-8))
+ r.s |= 1 << i;
+ for (int i = 0; i < ras; i++) {
+ int j = i+ras;
+ if (c[j/8] & 1 << (j%8-8))
+ r.d |= 1 << i;
+ }
+ r.i = c[is-1] & 1 << 6;
+ r.p = c[is-1] & 1 << 7;
+ return r;
+ }
+ void Ov::deserialize (istream & v = cin, unsigned int o = 0) {
+ string c((istreambuf_iterator<char>(v)), istreambuf_iterator<char>()); // eof
+ unsigned int s = c.size();
+ if (s % 2)
+ throw NotAligned;
+ for (unsigned int i = 0; i < s; i += is) {
+ pm[o+i/s] = deserialize(c.c_str()+i);
+ }
+ }
+}
+int main (void /* int argc, char ** argv */) {
+ clog << "OV stands for OB (One Bit) VM (Virtual Machine)." << endl
+ << "Stanard input is ready to accept a binary." << endl;
+ Ov ov;
+ ov.deserialize();
+ return 0;
+}
diff --git a/main.hpp b/main.hpp
new file mode 100644
index 0000000..f587fab
--- /dev/null
+++ b/main.hpp
@@ -0,0 +1,151 @@
+#include <vector>
+#include <queue>
+namespace ov {
+ using namespace std;
+ enum opts { // opts are pas-1 bits aftr last ram ptr (2**ras)-1 and can be w/r from prg
+ DEFAULT,
+ BUFOUT, // output from program in VM is always possible and no blocks
+ BUFIN, // input to program in VM is always possible and no blocks
+ BUFCLR, // clears whatever is in the buffer
+ OPTSLEN
+ }; // read those last bits with the COPY instruction with source/dest on last pointer
+ enum iobits {
+ INA, // input available, clear when read
+ IN, // input bit
+ OUTA, // output available, set when have output, cleared by VM when wrote
+ OUT, // output bit
+ IOLEN
+ };
+ enum instrs {
+ COPY,
+ NAND
+ };
+ struct instr { // deserializirana inštrukcija
+ unsigned int s = 0; // source
+ unsigned int d = 0; // destination
+ bool i = 0; // instruction
+ bool p = 0; // enobitni padding, lahko za metainštrukcije
+ }; // privzeto inicializiran na NOOP inštrukcijo
+ template<typename value_type, typename index_type, class Memory> class Mmu {
+ private:
+ class Cell {
+ private:
+ friend class Mmu;
+ Mmu & mmu;
+ index_type index;
+ Cell (Mmu & mmu, index_type index)
+ : mmu(mmu), index(index) {}
+ public:
+ operator value_type (void) {
+ return mmu.memory.peek(index);
+ }
+ value_type operator= (value_type value) {
+ mmu.memory.poke(index, value);
+ return value;
+ }
+ };
+ public:
+ Memory & memory;
+ Mmu (Memory & memory) : memory(memory) {}
+ Cell operator[] (index_type index) {
+ return Cell(*this, index);
+ }
+ value_type & operator() (index_type index) {
+ return memory.peek(index);
+ }
+ };
+ class Ov;
+ template<typename value_type, typename index_type> class Ram {
+ private:
+ vector<value_type> storage;
+ public:
+ Ov * ov;
+ value_type peek (index_type);
+ void poke(index_type, value_type);
+ Ram (Ov *);
+ };
+ /* v Program (memory) bi lahko uporabili metainštrukcije (tisti padding bit) v
+ * vsaki inštrukciji in v metainštrukcijah reprezentirali assembly org (lokacijo).
+ * s tem bi lahko imeli npr. 128 biten program counter in s tem zelo preproste jumpe,
+ * ne bi pa bilo treba narediti 2^128 vektor in posledično binarno datoteko.
+ * Tak način bi bilo verjetno težko implementirati na dejanski strojni opremi,
+ * tukaj pa bi v enem passu čez cel deserializan program memory zaznali te org
+ * metainštrukcije in naredili neko tabelo oziroma prevajalnik program counterja
+ * v lokacijo v vektorju. problem je, da bi se morala prevajalska tabela vsakič znova
+ * regenerirati, ko spreminjamo program memory.
+ * */
+ template<typename value_type, typename index_type> class Program {
+ private:
+ vector<value_type> storage;
+ public:
+ Ov * ov;
+ value_type & peek (index_type addr) {
+ return storage[addr];
+ }
+ void poke (index_type addr, value_type val) {
+ storage[addr] = val;
+ }
+ Program (Ov *);
+ };
+ class Ov {
+ private:
+ public:
+ queue<bool> inbuf;
+ queue<bool> outbuf;
+#define IS2RAS(is) ((is*8-2)/2)
+ class ras { // IS
+ private: // IT
+ public: // REALLY
+ unsigned short int & is; // THIS
+ operator unsigned short int (void) { // HARD
+ return IS2RAS(is); // TO
+ } // DEFINE
+ ras (class Ov * ov) : is(ov->is) {} // A
+ }; // GETTER
+ ras ras{this}; // FUNCTION
+ class rs {
+ private:
+ public:
+ unsigned short int & is;
+ operator unsigned int (void) {
+ return 1 << IS2RAS(is);
+ }
+ rs (class Ov * ov) : is(ov->is) {}
+ };
+ rs rs{this};
+ class ps {
+ private:
+ public:
+ unsigned short int & pas;
+ operator unsigned int (void) {
+ return 1 << pas;
+ }
+ ps (class Ov * ov) : pas(ov->pas) {}
+ };
+ ps ps{this};
+ unsigned short int is;
+ unsigned short int pas;
+ bool io[IOLEN];
+ bool opts[OPTSLEN];
+ unsigned int pc = 0; /* where the program starts --- at zero or where? */
+ Ram<bool, unsigned int> rstor{this};
+ Mmu<bool, unsigned int, class Ram<bool, unsigned int>> ram{rstor};
+ Program<struct instr, unsigned int> pstor{this};
+ Mmu<struct instr, unsigned, class Program<struct instr, unsigned>> pm{pstor};
+ Ov (unsigned short int is = 2, unsigned short int pas = 16)
+ : is(is), pas(pas) {
+ for (int i = 0; i < OPTSLEN; i++)
+ opts[i] = 0;
+ for (int i = 0; i < IOLEN; i++)
+ io[i] = 0;
+ }
+ void step (void);
+ bool out (void);
+ char outc (void);
+ void in (bool);
+ void inc (char);
+ struct instr deserialize (const char *);
+ void deserialize (istream &, unsigned int);
+ void pd (ostream); // print debug
+ };
+}