Design a One Instruction Set Computer!

32

9

Notice: I'm willing to give a bounty to any answer that I find interesting.

Your challenge is to design a Turing-complete one instruction set computer (OISC):

An OISC is an abstract machine that uses only one instruction – obviating the need for a machine language opcode. With a judicious choice for the single instruction and given infinite resources, an OISC is capable of being a universal computer in the same manner as traditional computers that have multiple instructions.

Here are some examples of single commands that make a Turing-complete OISC.

Rules:

You must provide an interpretation or proof thereof

You must provide an interpreter for your language. This interpreter should only be restricted by memory/time (e.g. must have no user-imposed restrictions). If you do not provide an interpreter for your language (for whatever reason other than laziness) you must prove that it is possible for one to be written. An interpreter must be possible.

You must prove its Turing-completeness

You must include a formal proof that your language is Turing-complete. A simple way to do this is by proving that it can interpret or have the same behavior as another Turing-complete language. The most basic language to interpret would be Brainf**k.

For example, a normal language that has all the same commands as Brainf**k (and the same lack of user-imposed memory restrictions) is Turing-complete because anything that can be implemented in Brainf**k can be implemented in the language.

Here is a list of very simple-to-implement Turing-complete languages.

Additional OISC requirements

  • This OISC should only have one instruction - it cannot have multiple instructions with one of them making it Turing-complete.

  • Your OISC may use any syntax you like. You should define in your answer what is instruction, what is data, and what is a no-op (e.g. whitespace). Be creative!

  • Arguments do not just need to be integers. For example, /// is a beautiful example of a Turing-complete OISC.

  • How and if input and output are taken and given are left up to you. Most OISCs implement I/O via specific memory locations, but there may be other ways to do so, and you are encouraged to find one.

  • A valid answer must provide some example code in your OISC, either by including it in the post or linking to a simple challenge solved in the language.

Voting

Voters, please remember not to upvote boring submissions. Examples:

  • Lenguage-equivalents
  • An implementation of an existing OISC (answerers, please create your own!)
  • An "OISC" in which the first argument specifies a command to call (example)

However, you should upvote interesting, creative submissions, such as:

  • An OISC based off of a mathematical equation
  • A Turing-complete ZISC based on a neural network
  • An OISC in which output I/O happens in other ways than certain memory locations

Winning

As with , the answer with the most votes wins! Good luck!

MD XF

Posted 7 years ago

Reputation: 11 605

Sandboxed post and OISC Wikipedia page. – MD XF – 7 years ago

not sure /// is an oisc still, due to its print instruction. – Destructible Lemon – 7 years ago

@DestructibleLemon It doesn't have a print instruction. It only has one instruction, which prints as a side effect. – MD XF – 7 years ago

10What is an "instruction"? And how do we count them? – Post Rock Garf Hunter – 7 years ago

I wish I knew enough to do this, it looks fun. – NoOneIsHere – 7 years ago

1@NoOneIsHere i wish i knew enough just to vote xD – Brian H. – 7 years ago

2I downvoted this. I think this is a very interesting idea, but you don't explain exactly what an OISC is and how to confirm something is one. I made BF an OISC, but that is clearly against the spirit of the question, but technically valid. – NoOneIsHere – 7 years ago

1@MDXF i don't think you get ///: it has a substitution command, and it has print commands, which is not just a side effect of the substitution command – Destructible Lemon – 7 years ago

@DestructibleLemon no it doesn't have a print command, it just prints everything left after substitution at the end of the program. – MD XF – 7 years ago

@MDXF http://esolangs.org/wiki////#Description

– Destructible Lemon – 7 years ago

1@NoOneIsHere Because [tag:popularity-contest]. Yes, it's valid, but it has poor score (vote tally), so it won't win. – user202729 – 7 years ago

1@user202729 Yeah, but just the fact that it is impossible to make sure a language truly has only 1 instruction makes me feel that the challenge is under-speficied. – NoOneIsHere – 7 years ago

Can one mandate that several specific values, [0, 1, and -1], are stored in memory locations? – user230118 – 7 years ago

@user230118 yes, that's fine. – MD XF – 7 years ago

Since I've voted to close this as unclear, I thought it would be a good idea to make explicit why. As per my previous comment there is no explanation as to how one can count the size of the instruction set. Without this definition I don't know how we can determine if an answer is a valid one. – Post Rock Garf Hunter – 7 years ago

@WheatWizard Can you think of an objective definition then? – MD XF – 7 years ago

@MDXF I don't even really understand what you want out of a definition, mostly because I'm unclear as to what you think is an OISC. If had an idea for a definition I would have already suggested it, but I don't know what qualities you want out of a definition. – Post Rock Garf Hunter – 7 years ago

Is MOV-like languages not real OISC? – l4m2 – 7 years ago

Answers

21

XOISC

This OISC is based on Fokker's X-combinator which is defined as follows:

X=λf .f (λg h x .g x (h x)) (λa b c .a)

If we acknowledge the fact that the SKI-calculus is Turing complete the above $X$-combinator is Turing complete as well. This is because $S$, $K$ and $I$ can be written in terms of $X$, like this:

S=X (X X)K=X XI=S K K=X (X X) (X X) (X X)

How XOISC works

Internally XOISC has an (initially empty) stack, from there the instruction taking $n$ as an argument does the following:

  • Pop $n$ elements (functions $ f_1 \dots f_N$) from the stack, push $f_1\ (f_2\ (\dots (f_N\ X) \dots ))$

Once there are no more instructions left XOISC will push all command-line arguments (if any) to the stack, for example:

[s1,, sMstack before, a1,, aNarguments]

The final computation will be $(\dots ((\dots (s_1\ s_2) \dots)\ s_M)\ a_1) \dots) a_N $.


Since the one instruction in XOISC takes only one argument (memory offset) there is no reason to even use a name for that instruction. So a valid source file will constitute solely of integers separated by newlines or whitespaces, like for example:

0 0 2 0 1 0 1

Try it online!

Example

Let's take the above example (stack growing to the right):

0pop 0 and apply (ie. push single X):[X]0again simply push X:[X, X]2pop 2 (a,b) and push a (b X):[X (X X)]0simply push X:[X (X X), X]1pop 1 (a) and push a X:[X (X X), X X]0simply push X:[X (X X), X X, X]1pop 1 (a) and push a X:[X (X X), X X, X X]

Finally evaluate the stack: $((X\ (X\ X))\ (X\ X))\ (X\ X)$ or with less parentheses $X\ (X\ X)\ (X\ X)\ (X\ X)$ which we recognize as the good old $S\ K\ K$ identity function.

Turing completeness

Proof idea

For XOISC to be Turing complete we need to be able to translate any (valid) interleaving of parentheses and $X$ combinators. This is possible because when popping, applying and pushing it does so in a right-associative manner (function application is left-associative).

To translate any such $X$ expression there is an easy way to do so: Always pop as many elements such that from the beginning of the current level of parentheses there will only be one element left.

As an example, the previously used expression: $((X\ (X\ X))\ (X\ X))\ (X\ X)$

  • to get $X$, we simply need a 0
  • next we're in a new level of parentheses, so we again only need a 0
  • now two parentheses close, so we need to pop 2 elements: 2
  • again we're in a new level of parentheses, so we need a 0
  • two parentheses, close so again a 2
  • and the same again

So we end up with a different (yet semantically equivalent) XOISC-program:

0 0 2 0 2 0 2 Try it online!

If we stay with this strategy we can easily transform any expression consisting of $X$ combinators to an XOISC program which only leaves a single function on the stack.

Formal proof

Given that the SKI-calculus is Turing complete, we need to show two things:

  1. the $X$-combinator is a basis for the SKI-calculus
  2. XOISC is able to represent any expression formed with the $X$ combinator

The first part - proving the three equalities in the introduction - is very tedious and space consuming, it's also not very interesting. So instead of putting it in this post, you can find here*.

The second part can be proven by structural induction, though it's easier to prove a slightly stronger statement: Namely, for any expression formed by $X$-combinators there is a program that will leave that expression as a single expression on the stack:

There are two ways of constructing such an $X$ expression, either it's $X$ itself or it's $f\ g$ for some expressions $f$ and $g$:

The former one is trivial as 0 will leave $X$ on the stack as a single expression. Now we suppose that there are two programs ($\texttt{F}_1 \dots \texttt{F}_N$ and $\texttt{G}_1 … \texttt{G}_K$) that will leave $f$ and $g$ as a single expression on the stack and prove that the statement holds for $f\ g$ as well:

The program $\texttt{F}_1 \dots \texttt{F}_N\ \texttt{G}_1 \dots \texttt{G}_{K-1}\ (\texttt{G}_K + 1)$ will first generate $f$ on the stack and then it will generate $g$ but instead of only popping parts of $g$ it will also pop $f$ and apply it, such that it leaves the single expression $f\ g$ on the stack. ∎

Interpreter

Inputs

Since the untyped lambda calculus requires us to define our own data types for everything we want and this is cumbersome the interpreter is aware of Church numerals - this means when you supply inputs it will automatically transform numbers to their corresponding Church numeral.

As an example here's a program that multiplies two numbers: Try it online!

You can also supply functions as arguments by using De Bruijn indices, for example the S combinator \\\(3 1 (2 1)) (or λλλ(3 1 (2 1))). However it also recognizes the S, K, I and of course X combinator.

Output

By default the interpreter checks if the output encodes an integer, if it does it will output the corresponding number (in addition to the result). For convenience there's the -b flag which tells the interpreter to try matching a boolean instead (see the last example).

Assembler

Of course any low-level language needs an assembler that converts a high-level language to it, you can simply use any input (see above) and translate it to a XOISC-program by using the -a flag, try it online!**


* In case the link is down, there's a copy as HTML comment in this post.

** This results in a program that tests for primality, try it online!

ბიმო

Posted 7 years ago

Reputation: 15 345

1Is there a reason you picked the X combinator instead of the Iota combinator? – Esolanging Fruit – 7 years ago

1@EsolangingFruit: Yeah, there are several other options as well, in the end I opted for that one because it uses the least applications to build SK. It seemed like it would perform the best (tbh I haven't done a comparison myself). – ბიმო – 7 years ago

1Btw. there is a nice comparison of several combinators in the linked paper if you're interested. – ბიმო – 7 years ago

19

Draw

Draw is an OISC acting on a 2D grid, marking squares in a manner similar to the Wang B-machine. However, to keep the language as simple and OISC-y as possible, all instructions (of which there are a grand total of one) mark the square just stepped on, and, in order to be able to halt, stepping on a marked square terminates the program.

The program consists of a sequence of lines containing a line identifier (arbitrary string not including # or whitespace), two integers (x and y) and two more line identifiers (a and b).

The program runs as follows:
Starting at the line identified as start with the pointer pointing to position (0, 0), move the pointer by the amount given by x and y and mark the square the pointer is now on (unless the square is already marked, in which case execution terminates). Then, jump to line a if at least one of the directly adjacent squares is also marked, and to line b otherwise.

Interpreters are encouraged to output the final result of the grid as some sort of image, canvas, etc.

Turing-Completeness

Draw is Turing-complete as it is possible to compile a modified version (called Alternate) of a Minsky machine into the language.

Alternate acts similarly to a two-counter Minsky machine, but there is a large restriction placed on the commands: commands must alternate between targeting the first and second counter. To get around this modification, an additional command has been added: nop. This command doesn't change the targeted counter at all, which makes it possible to "pad" consecutive changes to one counter, satisfying the restriction outlined above. This also means that the register that is to be modified doesn't have to be given and, for any given instruction, can be directly inferred from the instructions from which execution can jump to it.

Example: this Minsky machine

1 inc A 2
2 inc A 3
3 dec A 3 4
4 halt

turns into this Alternate program:

1 inc 2
2 nop 3
3 inc 4
4 nop 5
5 dec 6 8
6 nop 5
7 halt
8 halt

This restriction is necessary due to the way that the eventual Draw program handles registers, which is to say that it doesn't differentiate between them at all. Instead, the Draw program simply copies the register that hasn't been changed by the preceding instruction, modifying it according to the instruction being executed.

Then, the Alternate program is directly translated into Draw as follows:

The program starts with this block.

start 0 0 a a
a 3 0 b b
b -3 1 c c
c 3 0 d d
d -3 2 e e
e 3 0 f f
f 3 -3 i1_a i1_a

inc, dec and nop are translated in almost the same way as each other. In all cases, there is no difference between changing the first or the second register (as explained above). Here is an increment, equivalent to inc 2:

i1_y 0 -2 i1_z i1_y
i1_z 3 -1 i1_a i1_a
i1_a -5 1 i1_b i1_b
i1_b 0 2 i1_c i1_c
i1_c 0 2 i1_d i1_e
i1_d 0 2 i1_d i1_f

i1_e 5 0 i2_z i2_y
i1_f 5 0 i2_z i2_y

Change the numbers in the i1_x parts to the index of the current instruction, and in the i2_x parts to the index of the next instruction to be executed.

The nop instruction can be translated as such:

i1_y 0 -2 i1_z i1_y
i1_z 3 -1 i1_a i1_a
i1_a -5 1 i1_b i1_b
i1_b 0 2 i1_c i1_c
i1_c 0 2 i1_d i1_e
i1_d 0 2 i1_d i1_f

i1_e 5 -2 i2_z i2_y
i1_f 5 -2 i2_z i2_y

This is a decrement:

i1_y 0 -2 i1_z i1_y
i1_z 3 -1 i1_a i1_a
i1_a -5 1 i1_b i1_b
i1_b 0 2 i1_c i1_c
i1_c 0 2 i1_d i1_e
i1_d 0 2 i1_d i1_f

i1_e 5 -2 i3_z i3_y
i1_f 5 -4 i2_z i2_y

i3_x refers to the instruction to be called if the counter is already 1.

Halt:

i1_y 0 0 0 0
i1_z 0 0 0 0

Change labels appropriately and simply chain everything together. Doing this for the example from above gives the Draw program in the repository from above.

Interpreters

There are currently two interpreters, both written in Python. They can be found on Draw's GitHub repository.

  1. draw.py: This interpreter is meant for the command line and takes the program source as an argument. After every step, it outputs the command that was executed and the location of the instruction pointer; after the program halts, it prints the number of marked cells.
  2. draw_golly.py: This version uses Golly for exactly the wrong purpose easier graphical output, taking the source via a popup box when starting the script. Golly can be a little finicky with Python, so make sure you have Python 2 installed (and don't mix 32-bit Golly with 64-bit Python or vice versa). Output is provided via Golly's builtin cell grid.

The following image is an example for output from the second interpreter. Running the example program in the repository gives this (or similar):

ivzem

Posted 7 years ago

Reputation: 1 129

1Amazing! Congrats on finding a very unique way to do the challenge. – MD XF – 7 years ago

Your language doesn't need to halt at all to be turing complete. Rule 110 doesn't terminate, but it's turing complete nonetheless. – Akangka – 6 years ago

+1 for Golly the Best Cellular Automata Simulator Ever. – HighlyRadioactive – 5 years ago

15

-3

Here's the gist.

Memory

Memory is a map of tapes, where the keys are strings and values are arbitrary-sized integers.

Additionally, there is a set of labels, where the program can jump to.

There is a stack, which contains the operands, which are strings.

There is an offset, which controls where in the memory's tapes it can access.

The One Instruction

-. First, it pops a string LABEL off the stack. If that LABEL is undefined as a label, it defines the label, and clears the source of that label (i.e. where it was pushed from) and the current instruction. Otherwise, it performs the following calculation, using the top two values, A and B:

if mem[A] < mem[B]:
    jump to LABEL
if mem[A] != mem[B]:
    mem[A]--
else:
    mem[B]++

Note that if there are excess arguments or insufficient arguments, the program will error out, showing the program's state.

The offset can be modified by accessing the value of ..

Example code

X-

i i X-
i i X-
i i X-
i i X-
i i X-
i i X-
i i X-

This sets variable i to 7, by incrementing 7 times.

X-

i i X-
i i X-
i i X-
LOOP-
    a a X-
    a a X-
    j i LOOP-

This multiplies i+1 by the constant 2.

Proof of Turing Completeness

Disregarding C++'s int sizes (that is, assuming they are infinite), -3 is Turing Complete by reduction to 3-cell brainfuck. I can disregard this size because there can be written an interpreter for -3 on a computer with infinite memory that has arbitrarily-large cells.

I also believe that any BCT can be written as a -3 program.

Conor O'Brien

Posted 7 years ago

Reputation: 36 228

As I love to improve my content, please, an explanation concerning the down vote would be appreciated – Conor O'Brien – 7 years ago