Current version 0.2.0
Have you ever wanted to write code using domino pieces? No?
Well, now you can! Introducing DominoScript!
A recreational stack-oriented concatenative two-dimensional non-linear self-modifying int32-based esoteric programming language that uses the dots on domino pieces to represent code.
This repository contains the reference implementation written in TypeScript as well as all the documentation and examples for the language.
It's still very much a work-in-progress. Not everything is fully fleshed out yet.
Feel free to open issues for clarification requests, feature suggestions, bugs etc. I am grateful for any interest and help in eliminating bugs, edgecases and improve the documentation. I'd love for people to make their own Interpreters or Compilers and will link to all of them. Just be warned about potential breaking changes this early on!
-
Recreational Esolang
: This isn't a serious programming language. I got inspired after watching "The Art of Code" by Dylan Beattie where I discovered "Piet" and eventually went down the esolang rabbit hole. I wanted to create a language that is not only weirdly powerful but can also look good when hanged on a wall. -
Stack-Oriented
: There is a global data stack that all instructions operate on. Internally every item on the stack is a signed 32-bit integer. Strings are just null-terminated sequences of integers representing Unicode char codes. Floats are not supported. No other data structures exist. -
Concatenative
: DominoScript at its core is just another concatenative reverse-polish language. The following code:0—1 0—5 0—1 0—6 1—0 0—3 1—2 5—1
is the same as5 6 + dup * .
in Forth. -
Two-Dimensional
: The code is represented on a rectangle grid. The instruction pointer can move in any cardinal direction. One domino takes up 2 cells on the grid. Direction changes are performed by placing dominos in a certain way (IP always moves from one half to the other half of the same domino) as well as the current Navigation Mode. -
Self-Modifying
: The code can override itself similar to befunge. -
Òbfuscated
: Because all code is represented using domino pieces, reading it is somewhat like reading machine code. To "de-obfuscate" it you would need to replace the domino pieces with their corresponding instructions and literal values. The following:NUM 5 NUM 6 SUM DUPE MULT NUMOUT
is a readable pseudocode representation of DominoScript.
Be warned that the interpreter is still in its early stages and might not always work as expected.
The reference interpreter is here in this repo. See the here for details.
If you want to use dominoscript via the command line, you can install it globally like this:
npm install -g dominoscript
Then you can run it like this:
dominoscript path/to/your/file.ds
Or you can use npx to run it without installing it:
npx dominoscript path/to/your/file.ds
Eventually there will be an online editor where you can write and run it directly in the browser.
Maybe even a repository of user submitted DominoScript programs.
DominoScript by default uses Double-Six (aka D6
) dominos to represent code. Double-six here means that each domino has 2 sides with up to 6 dots on each side.
Everything is either:
- an instruction
- a number literal
- or a string literal
By default, everything will be parsed using base7. This behaviour can be changed using the BASE instruction. This means that with a higher base you can use dominos with more dots to represent larger numbers with fewer dominos.
- The grid is a rectangle of cells which can contain domino pieces.
- The grid can contain up to 65408 cells (soft limit)
- One domino takes up 2 cells and can placed horizontally or vertically.
- The top-left cell is address 0. The bottom-right cell is address
width * height - 1
. - When playing domino game variants you can usually place pieces "outside" the grid when both sides have the same number of dots: 🁈🁳🁀 - this is not allowed in DominoScript (Maybe in future versions but for now not worth the extra complexity)
Each cell needs to be indexable using an int32
popped from the stack, so in theory you could have something crazy like a 300k rows and columns. However, the interpreter will likely not be able to handle that. The artifical limit I decided on for now is a total of 65408 cells. That allows a square grid of 256x256
or various rectangular grids like 64x1024
, 128x512
, or 949x69
as long as the total cell count is 65408 or less. This limit will likely be configurable in future versions.
A text based format is used to represent domino pieces.
This format is used as source code. At the beginning it will be the only way to write DominoScript until a visual editor is created that shows actual dominos. Eventually I want to be able to convert images of real dominos on a (reasonably sized) grid into the text format.
- The digits
0
tof
represent the dots on half of a domino. To indicate an empty cell, use a dot.
- The "long hyphen" character
—
indicates a horizontal domino (regular hyphen-
also accepted to make it easier to type). It can only appear on even columns and odd rows. - The "pipe" character
|
indicates a vertical domino. It can only appear on odd columns and even rows. - Any other line before and after the actual code is ignored.
- It is just a text format, so the file extension doesn't matter for now. You can make it
.md
and comment using markdown if you want! See example
Example:
TITLE
=====
You can write the soure code as a normal text file (.ds extension recommended) or as a .md file with markdown comments like here.
Be aware of the following rules:
> 1. You cannot start a non-code line with a dot `.`
> 2. You cannot start a non-code line with a number `0 to f`
> 3. You cannot comment within the code. Only above and below it.
For comments starting with any non-allowed character, add a space or any other allowed char before it.
## DominoScript
The below code NO-OPs forever because
The IP can always move to a new domino
. . . . . . . .
. 6 6 6—6 6 6 .
| | | |
. 6 6 6 6 6 6 .
| |
. 6—6 6 6 6—6 .
. 6—6 6—6 6—6 .
. . . . . . . .
## Some Notes
Bla bla bla
When the source code is parsed it ignores everything except the actual code:
. . . . . . . .
. 6 6 6—6 6 6 .
| | | |
. 6 6 6 6 6 6 .
| |
. 6—6 6 6 6—6 .
. 6—6 6—6 6—6 .
. . . . . . . .
Which is the equivalent of these dominos:
The grid doesn't have to be a square but it must have a consistent number of columns and rows, otherwise an InvalidGridError
will be thrown before execution starts:
GOOD ✅ | BAD ❌ |
---|---|
|
|
Connecting to a domino half which is already connected results in MultiConnectionError
:
GOOD ✅ | BAD ❌ |
---|---|
|
|
Having a domino half that is not connected to anything results in MissingConnectionError
:
GOOD ✅ | BAD ❌ |
---|---|
|
|
Having a connection where 1 or both ends are empty results in a ConnectionToEmptyCellError
:
GOOD ✅ | BAD ❌ |
---|---|
|
|
- There is a single global stack that all instructions operate on.
- It only stores signed 32-bit Integers
- The interpreter will preallocate all the memory required to maintain the stack, therefore its size is limited to
512
items for now. (No particular reason for this rather small limit, it will likely be configurable in future versions)
Why not 64-bit integers?: No good reason really. I wanted to implement the first reference interpreter in typescript and since JS converts numbers to 32-bit when doing bitwise operations, I decided to just stick with 32-bit integers instead of having to split the lower and upper 32-bits for every bitwise operation. If there is demand, I will change the spec to support 64-bit ints but for now it is what it is.
DominoScript is a language where you cannot really tell what is going on just by looking at the code. It all depends on how the IP moves.
When the IP encounters a STR instruction, it will parse the next dominos as characters of a string. How that works exactly is explained in more detail in the description of the instruction.
It is important to understand that internally everything in DominoScript is represented as signed 32-bit integers and externally everything is represented by the dots on the domino pieces.
Internally strings are just null-terminated sequences of integers representing Unicode characters. It is your job as the developer to keep track of what items on the stack are numbers and what are characters of a string.
You can use any instruction on characters of a "string" but most of them will not distinguish between what is a number and a character. There are only 3 instructions which are specifically for handling strings: STR, STRIN, STROUT.
For convenience and clarity in examples I will often represent Unicode characters like this:
[..., 'NUL', 's', 'e', 'y']
But in reality the stack will store them as integers and look like this:
[..., 0, 115, 101, 121]
Floats don't exist in DominoScript. I'd suggest to scale up numbers by a factor of 10, 100, 1000 or whatever precision you need.
(I know that pico-8 uses 32-bits for numbers but treats them as 16.16 fixed point numbers. I am not quite sure if that is just a convention or if pico8's API actually treats them as fixed point numbers. I would like to eventually add some trigonometry instructions to DominoScripts "D9-mode" but unsure what the best way would be)
The instruction pointer (IP
) keeps track of the current cell address that will be used for the next instruction. Since DominoScript is 2D and non-linear, it isn't obvious where the IP will move to without understanding the fundamental rules and the Navigation Modes.
Before the program starts:
- the interpreter will scan the grid from top-left to top-right, move down and repeat until it finds the first domino.
- Upon reaching the first domino, the IP is placed at the address of the first found domino half.
- If no domino could be found, the program is considered finished.
During the program execution: The IP will adhere to the following rules:
-
Rule_01
: The IP moves in all cardinal directions, never diagonally. How dominos are parsed is all relative to that. For example the horizontal domino3—5
can be interpreted as the base7 number35
(IP moves eastwards) or53
(IP moves westwards). Same thing for vertical dominos. -
Rule_02
: The IP will always move from one half (entry) of the same domino to the other half (exit) of the same domino. -
Rule_03
: If the IP cannot move to a new domino, the program is considered finished. If aJUMP
happens to move to an empty cell, aJumpToEmptyCellError
is thrown and the program exits with a non-zero code -
Rule_04
: At the exit half of a domino, the IP will never move back to the entry half. It will always try to move to a new domino. That means there are 0 to 3 potential options for the IP to move. -
Rule_05
: When the IP needs to move to a new domino, it is possible that there are no valid moves despite there being dominos around. The Navigation Mode decides where the IP can and cannot move next.
In a nutshell Navigation Modes are predefined "behaviours" that follow a specific deterministic pattern. There are a lot of different nav modes. Some of them simple and logical, and others a bit more complex and chaotic.
Change navigation modes using the NAVM instruction.
First I'm gonna bombard you with some jargon:
Priority Directions (PDs)
: Primary, Secondary, TertiaryRelative Directions (RDs)
: Forward, Left, RightCardinal Directions (CDs)
: North, East, South, West
The Cardinal directions don't matter much. It is all about the direction in relation to the exit half of the current domino (If you ever did any kind of game dev you probably know the difference between world space and local space. It's kind of like that)
When the IP moves to a new domino, the half it enters to is called the "entry" while the other half is called the "exit". Now from the perspective of the exit half, the IP can potentially move in 3 directions: Forward, Left, Right. These are the Relative Directions (RDs).
Which direction it chooses depends on the current "Navigation Mode". Here are some of the most basic Nav Mode mappings:
index | Primary |
Secondary |
Tertiary |
---|---|---|---|
0 | Forward | Left | Right |
1 | Forward | Right | Left |
2 | Left | Forward | Right |
3 | Left | Right | Forward |
4 | Right | Forward | Left |
5 | Right | Left | Forward |
... | ... | ... | ... |
The "index" here is the argument for the NAVM
instruction but also refers to the current Navigation Mode
If we imagine the 6
to be the exit half, what will be the next domino the IP moves to?:
East | West | South | North |
---|---|---|---|
|
|
|
|
All 4 snippets are exactly the same code with the difference that they are all flipped differently. This is what I mean by the cardinal direction not mattering much in DominoScript.
index 0
the IP will move to1—1
(Primary, Forward)index 1
the IP will move to1—1
(Primary, Forward)index 2
the IP will move to2—2
(Primary, Left)index 3
the IP will move to2—2
(Primary, Left)index 4
the IP will move to3—3
(Primary, Right)index 5
the IP will move to3—3
(Primary, Right)
What if we remove the 1—1
domino? Where will the IP go to then?:
East | West | South | North |
---|---|---|---|
|
|
|
|
index 0
the IP will move to2—2
(Secondary, Left)index 1
the IP will move to3—3
(Secondary, Right)index 2
the IP will move to2—2
(Primary, Left)index 3
the IP will move to2—2
(Primary, Left)index 4
the IP will move to3—3
(Primary, Right)index 5
the IP will move to3—3
(Primary, Right)
And what if we remove both the 1—1
and the 2—2
domino?:
East | West | South | North |
---|---|---|---|
|
|
|
|
index 0
the IP will move to3—3
(Tertiary, Right)index 1
the IP will move to3—3
(Secondary, Right)index 2
the IP will move to3—3
(Tertiary, Right)index 3
the IP will move to3—3
(Secondary, Right)index 4
the IP will move to3—3
(Primary, Right)index 5
the IP will move to3—3
(Primary, Right)
Again, these are only the very basic navigation modes. See the reference for all the different modes and how they work.
DS isn't meant to be human readable but there are deterministic patterns that will make it much easier to follow along once you know them.
All of these patterns revolve around how the NUM
and STR
instructions behave differently than any other instruction.
Once you understand their differences, reading the rest of DominoScript is mostly a matter of keeping track of how the other instructions affect:
- the Stack (most of them do)
- the Instruction Pointer (JUMP, CALL, NAVM).
Here are the pattern and some examples:
PATTERN 1:
Look out for
0—1
and0—2
dominos.These are the opcodes for the
NUM
andSTR
instructions respectively. They are used to push numbers or strings to the stack so you will see them a lot.They are the ONLY 2 instructions that don't get their arguments from the stack but from the board.
PATTERN 2:
Look out for the first half of a domino right after a
NUM
instruction.It will decide how many more dominos will be part of the argument before the next instruction is executed.
The below code results in the number 6 being pushed and popped of the stack:
0—1 0—6 0—0
0—1
is theNUM
instruction (PATTERN 1)0—6
is the argument for NUM- first half is 0 which means no more dominos will follow and only second half is parsed as number (see PATTERN 2)
- Second half is 6 in both base7 and decimal so the number 6 is pushed to the stack
0—0
is the next instruction. We know that because the first half of previous domino told us that no more dominos will be part of the argument. (see PATTERN 2)
The below code results in the number 1000 being pushed and popped off the stack:
0—1 2—0 2—6 2—6 0—0
0—1
is theNUM
instruction (see PATTERN 1)2—0 2—6 2—6
is the argument for NUM representing 1000 in base7- the first half is 2 which means 2 more dominos will be part of the argument (see PATTERN 2)
- the remaining 2.5 dominos are parsed as 2626 in base7 which is 1000 in decimal.
0—0
is the next instruction. We know that because the first half of the domino afterNUM
told us that 2 more dominos will be parsed as part of the number so 3rd one after will be an instruction(see PATTERN 2).
PATTERN 3:
Look out for the first half of a domino right after a
STR
instruction.For the same reason as after a
NUM
instruction. It will decide how many more dominos will be part of the character before the next character is parsed.
PATTERN 4:
Look out for the NULL terminator
0—0
during aSTR
instruction.It indicates the the string is complete and will be pushed to the stack and that next domino will be a new instruction.
The below code results in the string "abc" being pushed to the stack.
0—2 1—1 6—6 1—2 0—0 1—2 0—1 0—0 0—1 0—6 0—0
0—2
is theSTR
instruction1—1 6—6
is the Unicode value for "a"1—2 0—0
is the Unicode value for "b"1—2 0—1
is the Unicode value for "c"0—0
is the null terminator and not thePOP
instruction as in the previous 2 examples. We know that becauseSTR
only ends once it encounters a0—0
(see PATTERN 4)0—1 0—6 0—0
is the code from the first example above. It will push the number 6 to the stack and then pop it off again.
All these patterns are deterministic but it doesn't mean that every 0—0
, 0—1
, 0—2
you see is a NULL terminator, a NUM
instruction or a STR
instruction respectively (as you can see in the examples above!)
It all depends on the context and how the IP moves:
- A
0—0
can be part of a number, the NULL terminator or thePOP
instruction - A
0—1
can be part of a number, theNUM
instruction or theADD
instruction if the IP moves in a different direction - A
0—2
can be part of a number, theSTR
instruction or theNOT
instruction if the IP moves in a different direction
The patterns are universal for all cardinal directions the Instruction Pointer can move in.
I only showed examples where the IP moves from left to right but you have to understand that the same domino can either mean the same thing or something completely different depending on the direction the Instruction Pointer moves in and what instructions precede it:
0—1 . 1—0 . 1 . 0 . . .
| |
. . . . . . 0 . 1 . . .
The base instruction set is designed to fit on a single "double-six" domino. It consists of up to 49 instructions and is shown below on a 7x7 matrix.
Keep in mind that if you change into a higher "base", you will need to use a different domino to represent the same opcode than shown in the images you see alongside each instruction. E.g. a NOOP in base7 is
6—6
but in base16 it would be3—0
.
0 | 1 | 2 | 3 | 4 | 5 | 6 | CATEGORY | |
---|---|---|---|---|---|---|---|---|
0 | POP | NUM | STR | DUPE | ROLL | LEN | CLR | Stack Management |
1 | ADD | SUB | MULT | DIV | MOD | NEG | _ | Arithmetic |
2 | NOT | AND | OR | EQL | GTR | _ | _ | Comparison & Logical |
3 | BNOT | BAND | BOR | BXOR | LSL | LSR | ASR | Bitwise |
4 | NAVM | BRANCH | LABEL | JUMP | CALL | IMPORT | WAIT | Control Flow |
5 | NUMIN | NUMOUT | STRIN | STROUT | _ | _ | _ | Input & Output |
6 | GET | SET | _ | BASE | EXT | TIME | NOOP | Misc |
(DominoScript isn't limited to these 49 instructions though. The way the language is designed, it can theoretically be extended to up to 1000 instructions)
Discards the top of the stack.
Errors:
- If the stack is empty, a
StackUnderflowError
is thrown.
Switch to "number mode". The first half of the next domino will indicate how many dominos to read as a number. Then the other halfs will all be read as base7 digits (in D6 mode) to form the number that will be pushed to the stack.
With 7 dominos, 13 out of 14 halfs are used for the number. You can theoretically represent a number much larger than the max int32 value. However, if the number exceeds the maximum int32 value, it will wrap around from the minimum value, and vice versa (exactly the same as when doing bitwise operations in JS --> (96889010406 | 0) === -1895237402
).
You might think that since internally numbers are int32s, that we parse from base7 to two's complement. That is not the case. We simple push the decimal version of the positive base7 number to the stack
For example:
0—0
represents the number0
in both deciamal and base70—6
represents the number6
in both decimal and base71—6 6—6
represents the number342
in decimal and666
in base72—6 6—6 6—6
represents the number16,806
in decimal and6,666
in base76—6 6—6 6—6 6—6 6—6 6—6
represents the number1,977,326,742
in decimal and66,666,666,666
in base7 (about 92.1% of the max int32 value)6—0 1—0 4—1 3—4 2—1 1—1 6—1
represents the number2,147,483,647
in decimal and104,134,211,161
in base7 (exactly the max int32 value)6—6 6—6 6—6 6—6 6—6 6—6 6—6
represents the number -1,895,237,402. WHY?: The actual decimal number the dominos represent is96,889,010,406
which is ~45x larger than the max int32 value. It wraps around about that many times before it reaches the final value.
What if numbers are read from the other direction?
1—1 1—1
,2—2 2—2 2—2
for example will be exactly the same numbers (216 in decimal) eastwards and westwards.1—2 3—1
when parsed backwards is1—3 2—1
and can therefore represent different numbers if the IP moves to the east or to the west.1—6 6—6
represents 666 in base7 (342 in decimal) but when parsed backwards the interpreter will raise anUnexpectedEndOfNumberError
. Remember that the first half of the first domino indicates how many more will follow. In this case it expects to read 6 more dominos but the number ends prematurely after 1 domino.
To push the number 10 and 5 to the stack you would use the following dominos:
- In pseudo code:
NUM 10 NUM 5
- In DominoScript:
0—1 1—0 1—3 0—1 0—5
0—1
is NUM1—0 1—3
is the number 13 in base7 which is 10 in decimal0—1
is NUM again0—5
is the number 5 in both base7 and decimal
To push the number -10 and -5 to the stack you would use the following dominos:
- In pseudo code:
NUM 10 NEG NUM 5 NEG
- In DominoScript:
0—1 1—0 1—3 1—5 0—1 0—5 1—5
0—1
is NUM1—0 1—3
is 13 in base7 which is 10 in decimal1—5
is NEG0—1
is NUM again0—5
is 5 in both base7 and decimal1—5
is NEG
Errors:
UnexpectedEndOfNumberError
is thrown when the IP cannot step as many times as indicated by the first half of the first domino.
With STR
you switch to "string mode" and can push multiple integers to the stack to represent Unicode characters.
The way the dominos are parsed to numbers is identical to NUM
: First half of first domino indicates how many more will follow for a single number.
The difference is that it doesn't stop with just one number. It will keep reading numbers until it encounters the NULL character represented by domino 0—0
.
Only once the interpreter does encounter the NULL character, will it push the characters to the stack in reverse order.
(Note: I decided to parse strings like this because I wanted a single int32 based stack and, out of all options I could think of, this one felt the least annoying. If you can think of better ways, I am open for suggestions!)
This is how you push the string "hi!"
to the stack and output it:
0—2 1—2 0—6 1—2 1—0 1—0 4—5 0—0 5—3
It equals the following pseudo code: STR "hi!" STROUT
0—2
is theSTR
instruction1—2 0—6
is the Unicode value 105 representing the characterh
1—2 1—0
is the Unicode value 105 representing the characteri
0—0 4—5
is the Unicode value 33 representing the character!
0—0
is the Unicode value for the NULL character which terminates the string.5—3
is the STROUT instruction. It will pop items from the stack, parse them as Unicode chars and once it encounters the NULL character, it will output the string to stdout all at once.
This is the resulting stack:
Imaginative | Reality |
---|---|
|
|
Keep in mind that the IP can move in 4 cardinal direction so the following variations would also push the string "hi!"
to the stack:
IP moves right to left:
3—5 0—0 5—4 0—1 0—1 2—1 6—0 2—1 2—0
IP moves in multiple directions:
0 . . . . 0 4—5
| |
2 . . . . 1 . 0
|
1 . . 2 1—0 . 0
| |
2 0—6 1 . . 3—5
Duplicate the top item on the stack.
Stack Before | Stack After |
---|---|
[a, b] |
[a, b, b] |
Pops one argument from the stack to be used as "depth".
- With a negative depth, the item at the top is moved to the nth depth
- With a positive depth, the item at the nth depth is moved to the top
With roll you can implement common stack operations like SWAP
and ROT
:
Roll Depth | Equivalent to | Stack Before | Stack After |
---|---|---|---|
-3 | - | [a, b, c, d] |
[d, a, b, c] |
-2 | ROTR | [a, b, c] |
[c, a, b] |
-1 | SWAP | [a, b] |
[b, a] |
0 | NOOP | [a] |
[a] |
1 | SWAP | [a, b] |
[b, a] |
2 | ROTL | [a, b, c] |
[b, c, a] |
3 | - | [a, b, c, d] |
[b, c, d, a] |
Pushes the number of items on the stack.
Removes all items from the stack.
Pops 2 numbers from the stack. The sum is pushed to the stack.
Pops 2 numbers from the stack. The result of numberA - numberB
is pushed to the stack.
Pops 2 numbers to multiply. The result is pushed to the stack.
Pops 2 numbers. The result of the division of numberA by numberB is pushed to the stack.
Keep in mind that DominoScript is integer based and any remainder is discarded.
Pseudocode:
NUM 5 NUM 3 DIV
is5 / 3
and equals1
NUM 5 NEG NUM 3 DIV
is-5 / 3
and equals-1
Pops 2 numbers. The remainder of division of numberA / numberB
is pushed to the stack.
When numberA is positive modulo behaves identical in most languages (afaik). However, there are some differences across programming languages when numberA is negative. In DominoScript modulo behaves like in JavaScript, Java, C++ and Go and NOT like in Python or Ruby!
Pseudocode:
NUM 5 NUM 3 MOD
is5 % 3
and equals2
NUM 5 NEG NUM 3 MOD
is-5 % 3
and equals-2
(in python, ruby and calculators it would equal1
)
Pops the top item off the stack. Negates it. Then pushes the negated version back onto the stack. Essentially a num * -1
operation.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Pops the top item off the stack. If it is 0
, it pushes 1
to the stack. Otherwise it pushes 0
.
Pops the top 2 items off the stack, performs the logical AND operation and pushes the result back onto the stack.
Pops the top 2 items off the stack, performs the logical OR operation and push the result back onto the stack.
Pops the top 2 items off the stack, compares them and pushes the result back onto the stack. If the items are equal, it pushes 1
to the stack, otherwise 0
.
Pops the top 2 items off the stack, compares them and pushes the result back onto the stack. If the first item is greater than the second, it pushes 1
to the stack, otherwise 0
.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Bitwise NOT. Pops the top item off the stack, inverts all bits and pushes the result back onto the stack.
Bitwise AND. Pops the top 2 items off the stack, performs bitwise AND and pushes the result back onto the stack.
Bitwise OR. Pops the top 2 items off the stack, performs bitwise OR and pushes the result back onto the stack.
Bitwise XOR. Pops the top 2 items off the stack, performs bitwise XOR and pushes the result back onto the stack.
Logical Shift Left. Performs the equivalent of argA << argB
and pushes the result back onto the stack.
Logical Shift Right. Performs the equivalent of argA >>> argB
and pushes the result back onto the stack.
Arithmetic Shift Right. Performs the equivalent of argA >> argB
and pushes the result back onto the stack.
Changes the Navigation Mode. The default Mode is 0
.
See Navigation Modes to see all possible nav modes and their indexes.
Like an IF-ELSE statement. It pops the top of the stack as a condition:
- When
true
: The IP will move LEFT - When
false
: The IP will move RIGHT
It ignores the current Navigation Mode, so you can be assured that it will always either go left or right.
Keep in mind that: all non-zero numbers are considered true. Only
0
is false!-1
,-2
etc. is true
Here we push 1 to the stack which will cause the IP to move LEFT:
. . . . . 6 . .
|
. . . . . 6 . .
0—1 0—1 4—1 . .
. . . . . X . .
|
. . . . . X . .
Here we push 0 to the stack which will cause the IP to move RIGHT:
. . . . . X . .
|
. . . . . X . .
0—1 0—0 4—1 . .
. . . . . 6 . .
|
. . . . . 6 . .
A label is like a bookmark or an alternative identifier of a specific Cell address. You can also think of it as a pointer. It can be used by the JUMP
, CALL
, GET
and SET
instructions.
Labels are probably not what you expect them to be.
- They are not strings, but negative numbers.
- They are auto generated and self decrementing:
-1
,-2
,-3
, etc. ...
Executing the LABEL instruction pops the address of the cell you want to label from the stack and assigns it to the next available negative number label.
The negative number label will NOT be pushed to the stack. First label will be -1
, second label will be -2
and so on. You need to keep track of them yourself.
For clarity, I'd generally recommend adding comments like the following to your source files:
## Label Mappings
| Label | Address | Function name |
|-------|---------|---------------|
| -1 | 340 | main |
| -2 | 675 | update |
| -3 | 704 | whatever |
It is not mandatory to use labels. The 4 mentioned instructions that can use them also work with addresses directly!
Labels are mandatory only in the following cases:
- Calling JS functions: The "official" js interpreter exposes an API where you can define functions in JS and call them from DominoScript via a label (Not implemented yet!)
- Calling imported functions: DominoScript will have an
IMPORT
instruction that allows source files to be imported into others. The imported functions can only be called via labels, so in that regard a label also acts like an export. (Not implemented yet!)
Moves the IP to a labeled address on the grid. If the IP cannot move anymore, the program will terminate.
If label is unknown it will throw an UnknownLabelError
.
Like the name suggests, it is similar to a function call.
Exactly like JUMP with one crucial difference: When it cannot move anymore, the IP will return to where it was called from instead of terminating the program.
Internally there is another stack that keeps track of the return addresses.
Pop a "string" from the stack to indicate the file name of the source file to import.
On import the interpreter will load the file and start running it until its Instruction Pointer cannot move anymore.
Labels defined in the imported file are accessible from the importing file. That means you can call functions from the imported file via the CALL
instruction.
If the importer file defined label before the import, the labels from the imported will have different identifiers. For example:
FileChild.ds
defines a label-1
.FileAParent.ds
defines labels-1
,-2
, then imports FileB.ds, then defines another label-6
.
The label -1
in FileChild.ds
will be -3
in FileAParent.ds
because labels are always auto decrementing. Why? Because it is the simplest way to avoid conflicts and be able to use labels internally and externally.
The data stack is shared between parent and all imported files. Apart from that they parent and child imports run in their own contexts. Imported files can have imports themselves but you should avoid circular dependencies.
If you import the same file into more than one other file, it will result in multiple instances of the imported file. This is not a problem as long as you are aware of it.
Pops the top item off the stack and waits for that many milliseconds before continuing.
(You could simulate a delay without using WAIT
using a 'busy loop' like in example 011_basic_game_loop but it is not recommended)
Prompt the user for a number. The user input will be pushed to the stack.
Pop the top item from the stack and output it to stdout.
Prompt the user for a string. The user input will be pushed to the stack as individual Unicode characters in reverse order.
So if the user inputs "yes"
, the stack will look like this:
[..., 0, 115, 101, 121]
For convenience you might often see the stack represented But remember that in reality it just stores int32s.
[..., 'NUL' 's', 'e', 'y']
Pops numbers (representing Unicode char codes) from the stack until it encounters a null terminator (number 0). It will then output the string to stdout.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Get the value of a single whole domino at a specific address on the grid.
Pops an item from the stack representing the address of the first domino half, then gets the number of dots on that first half and the other half. Out of both halves it forms a number the same way it does when parsing opcodes. The resulting decimal value will then be pushed to the stack.
For example:
0—6
: First half 0, second half 6 -> 6 in base7 -> 6 in decimal pushed to stack1—0
: First half 1, second half 0 -> 10 in base7 -> 7 in decimal pushed to stack6—6
: First half 6, second half 6 -> 66 in base7 -> 48 in decimal pushed to stack
Errors:
- If the address is out of bounds, an
InvalidAddressError
is thrown. - If the address references an empty cell the number -1 will be pushed to the stack. No error.
Sets the value of 2 cells (a whole domino) on the grid. This one is similar to the p
instruction in befunge used to modify the grid.
It requires 2 arguments from the stack:
- The address of the first cell (just like
GET
) - and then the value to set the domino to. 0-48 range is allowed for D6 mode. If
For example here we have a 10x3 grid:
Before SET | After SET |
---|---|
|
|
What has happened?:
0—1 1—0 6—0
We push the number 42 to the stack0—1 1—0 1—0
We push the number 13 to the stack (last domino wraps around on second line)6—1
We execute theSET
instruction- it pops 13 for the address
- it pops 42 for the value
The decimal number 42 is 60
in base7. The cell at address 13 is set to the value 6
, the cell at the address 12 is set to the value 0
:
But why is the set domino "backwards"? Because the IP was moving to the west when SET
was executed. Remember that the IP can move in all 4 cardinal directions, so there is no "backwards" or "forwards" in the traditional sense. The address argument indicates the first cell only. The other one depends on the direction the IP is moving.
Errors:
- If the address argument is out of bounds, an
InvalidAddressError
is thrown. - If the value argument is not within 0-48, an
InvalidDominoValueError
is thrown. - If the address of the other domino half is out of bounds, an
AddressError
is thrown.
Unmapped opcode. Will throw InvalidInstructionError
if executed.
Pops one number from the stack to use as the "base" for future parsing of dominos (opcodes, number literals, string literals)
By default, DominoScript uses double six (D6) dominos to represent everything, so the default base is 7.
The max cell value of half of a domino is always 1 less than the Base. So in base 7, the max value is 6. In base 10, the max value is 9. In base 16, the max value is 15 (aka f
).
If the number of dots on a domino half exceeds the base, it is clamped.
In below table you can see how the same domino sequence results in different decimal numbers depending on the base:
Domino Sequence | Base 7 (D6) | Base 10 (D9) | Base 16 (D15) |
---|---|---|---|
0—6 |
6 | 6 | 6 |
0—9 |
6 | 9 | 9 |
0—f |
6 | 9 | 15 |
1—6 6—6 |
342 | 666 | 1638 |
1—9 9—9 |
342 | 999 | 2457 |
1—f f—f |
342 | 999 | 4095 |
2—6 6—6 6—6 |
16806 | 66666 | 419430 |
2—9 9—9 9—9 |
16806 | 99999 | 629145 |
2—f f—f f—f |
16806 | 99999 | 1048575 |
With a higher Base, you have access to higher opcodes without needing to switch to extended mode.
Base | Opcode Range |
---|---|
7 | 0 to 48 |
10 | 0 to 99 |
16 | 0 to 255 |
While the opcode-to-instruction mapping never changes, the domino-to-opcode mapping is completely different in each base.
The below table shows how the domino 3—0
is mapped to different opcodes depending on the base.
Base | Opcode | Instruction |
---|---|---|
7 | 21 | BNOT |
8 | 24 | BOR |
9 | 27 | LSL |
10 | 30 | BRANCH |
11 | 33 | IMPORT |
12 | 36 | NUMIN |
13 | 39 | undefined |
14 | 42 | GET |
15 | 45 | BASE |
16 | 48 | NOOP |
Toggle extended mode on or off. If extended mode is active the interpreter will use 2 dominos instead of 1 for each instruction which extends the opcode range from 0-48 to 0-2400.
The range 0-1000 is reserved for "real" instructions.
The range 1001-2400 is used to call functions by label. It is essentially "Syntactic Sugar" to execute CALL with a label. Making function calls look like actual instructions.
To call a function with the label -1 you'd normally do 0—1 0—1 1—5 4—4
which is equivalent to NUM 1 NEG CALL
.
In extended mode you could do the same 0—0 0—1 0—1 0—0 1—5 0—0 4—4
BUT You can also do the exact same using 2—6 3—0
which is the opcode 1001 and is mapped to the label -1.
Pushes the milliseconds since program start to the stack.
Useful for things like a gameloop, animations, cooldowns etc.
No operation. The IP will move to the next domino without executing any instruction.
(If you have 10 NOOPs in a row it will do 10 steps without doing anything. Over time, the interpreter may optimize this and do an implicit jump to the end of the NOOP chain when it things you are within a loop and the navigation mode doesn't change)
(F=Forward, L=Left, R=Right)
There are 49
total navigation modes in DominoScript. This section is a reference for all of them.
Basic
: The IP will prioritize moving in specific directionsCycle
: The IP will prioritize moving in specific directions but the priority will change every cycle.Flip Flop
: The IP will alternate between two primary directions.
Out of three directions, the IP will prioritize moving to the one with the highest priority.
Index | Priorities | Domino -> |
---|---|---|
0 | Forward Left Right |
0—0 |
1 | Forward Right Left |
0—1 |
2 | Left Forward Right |
0—2 |
3 | Left Right Forward |
0—3 |
4 | Right Forward Left |
0—4 |
5 | Right Left Forward |
0—5 |
6 | RANDOM |
0—6 |
Out of two directions, the IP will prioritize moving to the one with the highest priority.
Index | Priorities | Domino -> |
---|---|---|
7 | Forward Left |
1—0 |
8 | Forward Right |
1—1 |
9 | Left Forward |
1—2 |
10 | Left Right |
1—3 |
11 | Right Forward |
1—4 |
12 | Right Left |
1—5 |
13 | RANDOM |
1—6 |
IP can only move in one direction.
Index | Only Direction | Domino -> |
---|---|---|
14 | Forward |
2—0 |
15 | Forward |
2—1 |
16 | Left |
2—2 |
17 | Left |
2—3 |
18 | Right |
2—4 |
19 | Right |
2—5 |
20 | RANDOM |
2—6 |
The direction with the highest priority becomes the least prioritized in the next cycle.
All 3 directions are available in all cycles.
Index | Cycle 1 | Cycle 2 | Cycle 3 | Domino -> |
---|---|---|---|---|
21 | F L R |
L R F |
R F L |
3—0 |
22 | F R L |
R F F |
L F R |
3—1 |
23 | L F R |
F R F |
R L F |
3—2 |
24 | L R F |
R F L |
F L R |
3—3 |
25 | R F L |
F L R |
L R F |
3—4 |
26 | R L F |
L F R |
F R L |
3—5 |
27 | (unmapped) | (unmapped) | (unmapped) | 3—6 |
The direction with the highest priority becomes the least prioritized in the next cycle.
Only 2 directions are available in a single cycle.
Index | Cycle 1 | Cycle 2 | Cycle 3 | Domino -> |
---|---|---|---|---|
28 | F L |
L R |
R F |
4—0 |
29 | F R |
R F |
L F |
4—1 |
30 | L F |
F R |
R L |
4—2 |
31 | L R |
R F |
F L |
4—3 |
32 | R F |
F L |
L R |
4—4 |
33 | R L |
L F |
F R |
4—5 |
34 | (unmapped) | (unmapped) | (unmapped) | 4—6 |
The direction with the highest priority becomes the least prioritized in the next cycle.
Only 1 direction is available in a single cycle.
Index | Cycle 1 | Cycle 2 | Cycle 3 | Domino -> |
---|---|---|---|---|
35 | F |
L |
R |
5—0 |
36 | F |
R |
L |
5—1 |
37 | L |
F |
R |
5—2 |
38 | L |
R |
F |
5—3 |
39 | R |
F |
L |
5—4 |
40 | R |
L |
F |
5—5 |
41 | (unmapped) | (unmapped) | (unmapped) | 5—6 |
The priority flip flops between 2 primary directions.
Index | Flip | Flop | Domino -> |
---|---|---|---|
42 | F |
L |
6—0 |
43 | F |
R |
6—1 |
44 | L |
F |
6—2 |
45 | L |
R |
6—3 |
46 | R |
F |
6—4 |
47 | R |
L |
6—5 |
48 | (unmapped) | (unmapped) | 6—6 |
Wiith DominoScript you can output Unicode characters to the console. Here is a lookup table for the ASCII range.
CHARACTER | UNICODE (Hex) | DECIMAL | BASE7 | DOMINO --> |
---|---|---|---|---|
NUL (null character) |
U+0000 | 0 | 0 | 0—0 |
SOH (start of heading) |
U+0001 | 1 | 1 | 0—1 |
STX (start of text) |
U+0002 | 2 | 2 | 0—2 |
ETX (end of text) |
U+0003 | 3 | 3 | 0—3 |
EOT (end of transmission) |
U+0004 | 4 | 4 | 0—4 |
ENQ (enquiry) |
U+0005 | 5 | 5 | 0—5 |
ACK (acknowledge) |
U+0006 | 6 | 6 | 0—6 |
BEL (bell) |
U+0007 | 7 | 10 | 1—0 1—0 |
BS (backspace) |
U+0008 | 8 | 11 | 1—0 1—1 |
HT (horizontal tab) |
U+0009 | 9 | 12 | 1—0 1—2 |
LF (line feed) |
U+000A | 10 | 13 | 1—0 1—3 |
VT (vertical tab) |
U+000B | 11 | 14 | 1—0 1—4 |
FF (form feed) |
U+000C | 12 | 15 | 1—0 1—5 |
CR (carriage return) |
U+000D | 13 | 16 | 1—0 1—6 |
SO (shift out) |
U+000E | 14 | 20 | 1—0 2—0 |
SI (shift in) |
U+000F | 15 | 21 | 1—0 2—1 |
DLE (data link escape) |
U+0010 | 16 | 22 | 1—0 2—2 |
DC1 (device control 1) |
U+0011 | 17 | 23 | 1—0 2—3 |
DC2 (device control 2) |
U+0012 | 18 | 24 | 1—0 2—4 |
DC3 (device control 3) |
U+0013 | 19 | 25 | 1—0 2—5 |
DC4 (device control 4) |
U+0014 | 20 | 26 | 1—0 2—6 |
NAK (negative acknowledge) |
U+0015 | 21 | 30 | 1—0 3—0 |
SYN (synchronous idle) |
U+0016 | 22 | 31 | 1—0 3—1 |
ETB (end of transmission block) |
U+0017 | 23 | 32 | 1—0 3—2 |
CAN (cancel) |
U+0018 | 24 | 33 | 1—0 3—3 |
EM (end of medium) |
U+0019 | 25 | 34 | 1—0 3—4 |
SUB (substitute) |
U+001A | 26 | 35 | 1—0 3—5 |
ESC (escape) |
U+001B | 27 | 36 | 1—0 3—6 |
FS (file separator) |
U+001C | 28 | 40 | 1—0 4—0 |
GS (group separator) |
U+001D | 29 | 41 | 1—0 4—1 |
RS (record separator) |
U+001E | 30 | 42 | 1—0 4—2 |
US (unit separator) |
U+001F | 31 | 43 | 1—0 4—3 |
CHARACTER | UNICODE (Hex) | DECIMAL | BASE7 | DOMINO --> |
---|---|---|---|---|
space | U+0020 | 32 | 44 | 1—0 4—4 |
! |
U+0021 | 33 | 45 | 1—0 4—5 |
" |
U+0022 | 34 | 46 | 1—0 4—6 |
# |
U+0023 | 35 | 50 | 1—0 5—0 |
$ |
U+0024 | 36 | 51 | 1—0 5—1 |
% |
U+0025 | 37 | 52 | 1—0 5—2 |
& |
U+0026 | 38 | 53 | 1—0 5—3 |
' |
U+0027 | 39 | 54 | 1—0 5—4 |
( |
U+0028 | 40 | 55 | 1—0 5—5 |
) |
U+0029 | 41 | 56 | 1—0 5—6 |
* |
U+002A | 42 | 60 | 1—0 6—0 |
+ |
U+002B | 43 | 61 | 1—0 6—1 |
, |
U+002C | 44 | 62 | 1—0 6—2 |
- |
U+002D | 45 | 63 | 1—0 6—3 |
. |
U+002E | 46 | 64 | 1—0 6—4 |
/ |
U+002F | 47 | 65 | 1—0 6—5 |
0 |
U+0030 | 48 | 66 | 1—0 6—6 |
1 |
U+0031 | 49 | 100 | 1—1 0—0 |
2 |
U+0032 | 50 | 101 | 1—1 0—1 |
3 |
U+0033 | 51 | 102 | 1—1 0—2 |
4 |
U+0034 | 52 | 103 | 1—1 0—3 |
5 |
U+0035 | 53 | 104 | 1—1 0—4 |
6 |
U+0036 | 54 | 105 | 1—1 0—5 |
7 |
U+0037 | 55 | 106 | 1—1 0—6 |
8 |
U+0038 | 56 | 110 | 1—1 1—0 |
9 |
U+0039 | 57 | 111 | 1—1 1—1 |
: |
U+003A | 58 | 112 | 1—1 1—2 |
; |
U+003B | 59 | 113 | 1—1 1—3 |
< |
U+003C | 60 | 114 | 1—1 1—4 |
= |
U+003D | 61 | 115 | 1—1 1—5 |
> |
U+003E | 62 | 116 | 1—1 1—6 |
? |
U+003F | 63 | 120 | 1—1 2—0 |
@ |
U+0040 | 64 | 121 | 1—1 2—1 |
A |
U+0041 | 65 | 122 | 1—1 2—2 |
B |
U+0042 | 66 | 123 | 1—1 2—3 |
C |
U+0043 | 67 | 124 | 1—1 2—4 |
D |
U+0044 | 68 | 125 | 1—1 2—5 |
E |
U+0045 | 69 | 126 | 1—1 2—6 |
F |
U+0046 | 70 | 130 | 1—1 3—0 |
G |
U+0047 | 71 | 131 | 1—1 3—1 |
H |
U+0048 | 72 | 132 | 1—1 3—2 |
I |
U+0049 | 73 | 133 | 1—1 3—3 |
J |
U+004A | 74 | 134 | 1—1 3—4 |
K |
U+004B | 75 | 135 | 1—1 3—5 |
L |
U+004C | 76 | 136 | 1—1 3—6 |
M |
U+004D | 77 | 140 | 1—1 4—0 |
N |
U+004E | 78 | 141 | 1—1 4—1 |
O |
U+004F | 79 | 142 | 1—1 4—2 |
P |
U+0050 | 80 | 143 | 1—1 4—3 |
Q |
U+0051 | 81 | 144 | 1—1 4—4 |
R |
U+0052 | 82 | 145 | 1—1 4—5 |
S |
U+0053 | 83 | 146 | 1—1 4—6 |
T |
U+0054 | 84 | 150 | 1—1 5—0 |
U |
U+0055 | 85 | 151 | 1—1 5—1 |
V |
U+0056 | 86 | 152 | 1—1 5—2 |
W |
U+0057 | 87 | 153 | 1—1 5—3 |
X |
U+0058 | 88 | 154 | 1—1 5—4 |
Y |
U+0059 | 89 | 155 | 1—1 5—5 |
Z |
U+005A | 90 | 156 | 1—1 5—6 |
[ |
U+005B | 91 | 160 | 1—1 6—0 |
\ |
U+005C | 92 | 161 | 1—1 6—1 |
] |
U+005D | 93 | 162 | 1—1 6—2 |
^ |
U+005E | 94 | 163 | 1—1 6—3 |
_ |
U+005F | 95 | 164 | 1—1 6—4 |
` |
U+0060 | 96 | 165 | 1—1 6—5 |
a |
U+0061 | 97 | 166 | 1—1 6—6 |
b |
U+0062 | 98 | 200 | 1—2 0—0 |
c |
U+0063 | 99 | 201 | 1—2 0—1 |
d |
U+0064 | 100 | 202 | 1—2 0—2 |
e |
U+0065 | 101 | 203 | 1—2 0—3 |
f |
U+0066 | 102 | 204 | 1—2 0—4 |
g |
U+0067 | 103 | 205 | 1—2 0—5 |
h |
U+0068 | 104 | 206 | 1—2 0—6 |
i |
U+0069 | 105 | 210 | 1—2 1—0 |
j |
U+006A | 106 | 211 | 1—2 1—1 |
k |
U+006B | 107 | 212 | 1—2 1—2 |
l |
U+006C | 108 | 213 | 1—2 1—3 |
m |
U+006D | 109 | 214 | 1—2 1—4 |
n |
U+006E | 110 | 215 | 1—2 1—5 |
o |
U+006F | 111 | 216 | 1—2 1—6 |
p |
U+0070 | 112 | 220 | 1—2 2—0 |
q |
U+0071 | 113 | 221 | 1—2 2—1 |
r |
U+0072 | 114 | 222 | 1—2 2—2 |
s |
U+0073 | 115 | 223 | 1—2 2—3 |
t |
U+0074 | 116 | 224 | 1—2 2—4 |
u |
U+0075 | 117 | 225 | 1—2 2—5 |
v |
U+0076 | 118 | 226 | 1—2 2—6 |
w |
U+0077 | 119 | 230 | 1—2 3—0 |
x |
U+0078 | 120 | 231 | 1—2 3—1 |
y |
U+0079 | 121 | 232 | 1—2 3—2 |
z |
U+007A | 122 | 233 | 1—2 3—3 |
{ |
U+007B | 123 | 234 | 1—2 3—4 |
| |
U+007C | 124 | 235 | 1—2 3—5 |
} |
U+007D | 125 | 236 | 1—2 3—6 |
~ |
U+007E | 126 | 240 | 1—2 4—0 |
The spec doesn't define a way to "catch" errors in a graceful way yet. For now, whenever an error occurs, the program will terminate and the interpreter will throw an error to stderr.
- SyntaxError: Unexpected token '{token}' at line {line}, column {column}
- InvalidGridError: Invalid grid. All lines containing code must be the same length (for now)
- MultiConnectionError: {type} connection at line {line}, column {column} is trying to connect a cell that is already connected
- MissingConnectionError: Non-empty cell at line {line}, column {column} does not have a connection
- ConnectionToEmptyCellError: Connection to an empty cell at line {line}, column {column}
- ConnectionToEmptyCellsError: There are connectors that are not connected to anything (Cannot give you the exact location of the error atm)
- InterpreterError: Something wrong with the Interpreter: {message}
- UnexpectedEndOfInputError: Unexpected end of input at line {line}, column {column}
- AddressError: Address '{address}' out of bounds
- InvalidLabelError: Label {name} is not a valid label
- StepToEmptyCellError: Trying to step from cell {currentAddress} to empty cell {emptyAddress}
- JumpToItselfError: Jumping to itself at address {address} is forbidden as it results in an infinite loop
- CallToItselfError:Calling to itself at address {address} is forbidden as it results in an infinite loop
- UnexpectedEndOfNumberError: Unexpected end of number at address {address}
- EmptyStackError: Cannot pop from an empty stack
- FullStackError: Cannot push to a full stack
- InvalidInstructionError: Invalid instruction opcode {opcode}
- InvalidNavigationModeError: Invalid navigation mode {mode}
- InvalidValueError: Invalid value {value}
- InvalidInputError: Invalid input {reason}
- MissingListenerError: NUMIN, NUMOUT, STRIN or STROUT instructions were called and the DominoScript "runtime" did not provide a way on how to handle input or output
A list of examples to help you understand the language better.
- Hello World minimal
- Hello World Commented
- Hello World 2D
- Loop Simple
- Loop using jump
- Loop using jump and label
- Call function by address
- Call function by label
- Recursion: Factorial
- Navigation Mode changes
- Basic game loop
- Number Input
- String Input
- Import script into another
- Call imported function
- Ansi clear screen
- Using delay
- Reverse String