Grobots are programmed in a simple stack-based language derived from Forth. This imitates RoboWar, but the language itself is much closer to Forth. These stack-based languages are good for this sort of application because they are very easy to implement, and easier to learn and use than other similarly low-level languages.
The Grobots language doesn't have a name yet; I will call it Grobocode here for lack of a better name. If you are used to RoboTalk, you will find it pleasantly powerful. If you are used to Forth, you will find it annoyingly underpowered. If you don't know any stack-based languages, you will just find it weird.
Like all Forth dialects, keeps intermediate results of calculations on a stack: operands are pushed onto the stack, and operators take their arguments from the stack. Expressions are written in postfix order - the operator comes after all its arguments. This may be unfamiliar, but fortunately it doesn't take long to get used to. What in a conventional language would be:
gamma = 1 / sqrt(1 - (v / c)^2)
is in Grobots:
1 v c / square - sqrt reciprocal gamma!
Here's how it works. Start with an empty stack, and execute the first instruction,
1
, which pushes a value on the stack.
stack:1
Thenv
, which pushes the value of that variable: stack:1 201285
Thenc
: stack:1 201285 299780
/
takes the top two arguments and leaves their quotient: stack:1 0.6714
square
takes one argument stack:1 0.45
-
stack:0.55
sqrt
stack:0.7416
reciprocal
stack:1.35
gamma!
stores the value into the variable of that name. (empty stack)
Try clicking on a robot and opening the Debugger window. Then pause the simulation and give the Step Brain command to slowly advance the brain. Watch the stack change.
There are instructions to operate on the stack, so you can do fancier things like using a result more than once, or shuffling things to be in the right order:
fibonacci: ;n -- f_n
dup 1 <= if drop 1 return then
dup 1 - fibonacci swap 2 - fibonacci +
return
See if you can understand how that works. (You'll need to look up a lot of the operators, and this may be confusing to trace since it's recursive.) n -- f_n
is a stack diagram, saying the newly defined fibonacci operator takes one argument (n) and returns one result (f_n).
The most confusing errors are stack underflow and overflow. Underflow means means you tried to remove more values from the stack than were actually there. For example, 2 +
will probably underflow, because +
needs two arguments. Overflow means you kept leaving values on the stack, so they piled higher and higher until the interpreter gave up.
Grobocode runs on a two-stack machine, like traditional Forth. There is a return stack, which is only used for return addresses, and a data stack, used for arguments and return values of operators.
The only data type is a 32-bit fixed-point number, with 12 bits of fraction (and 1 of sign and 19 of integer). The range is about +/- 524,288, and the precision 1/4096 (.00024). Integer values may be used as addresses.
Grobocode syntax is a sequence of words separated by white space. There are several kinds of words; suffixes are sometimes used to indicate which kind. Each word (except compile-time words and label declarations) compiles to one instruction.
Kind of word | Suffix |
---|---|
Primitive call | none |
Read variable (including constants, vector variables, and hardware variables) | none |
Write variable | !
|
Read label (put its address on the stack) | &
|
Call label | ^ (optional)
|
Declare label | :
|
Immediate number | none |
Semicolon makes a comment to the end of the line. This works everywhere, not just in code.
Variables and constants are defined somewhat clumsily by leaving the language and using reader tags. #var name initial-value
defines a variable, #vector name initial-x initial-y
defines a vector variable, and #const name value
defines a constant.
Hardware access words are not included here, but are listed on the hardware page.
Word | Stack diagram | Comments |
---|---|---|
nop | -- | Does nothing, of course. Used for padding in jump tables. |
Stack manipulation | ||
drop | a -- | Discard the top item on the stack. |
2drop | a b -- | Discard the top two items. |
nip | a b -- b | Discard the second item on the stack. |
rdrop | R: a -- | Discard the top item on the return stack. |
dropn | xn ... x2 x1 n -- | remove n items from the stack. |
swap | a b -- b a | Exchange the top two items. |
2swap | a b c d -- c d a b | Exchange the top two pairs of items. |
rot | a b c -- b c a | |
rrot | a b c -- c a b | |
dup | a -- a a | These all duplicate items that are on the stack. |
2dup | a b -- a b a b | |
tuck | a b -- b a b | |
over | a b -- a b a | |
2over | a b c d -- a b c d a b | |
stack | -- height | Return the number of items that were on the stack. |
stack-limit | -- limit | Return the maximum number of items that can be on the stack. |
pick | xn ... x2 x1 n -- xn ... x2 x1 xn | copy the nth item to the top of the stack. |
>r | a -- R: -- a | Move an item to or from the return stack. The item must be a valid address, so these instructions are not very useful. |
r> | -- a R: a -- | |
Branches | ||
jump | address -- | Continue execution from address. |
call | address -- R: -- pc | Push the address of the next instruction on the return stack, and continue execution from address. |
return | R: address -- | Remove address from the return stack and continue from it. |
ifg | flag address -- | Jump to address if flag is nonzero. |
nifg | flag address -- | Jump to address if flag is zero. |
ifeg | flag addr1 addr2 -- | Jump to addr1 if flag is nonzero or to addr2 if it is zero. |
ifc | flag address -- [R: -- pc] | Call address if flag is nonzero. |
ifec | flag addr1 addr2 -- R: -- pc | |
nifc | flag address -- [R: -- pc] | Call address if flag is zero. |
ifr | flag -- [R: address --] | Returns (to the top address on the return stack) if flag is nonzero. |
nifr | flag -- [R: address --] | Returns if flag is zero. |
Arithmetic etc. | ||
+ | a b -- a+b | |
- | a b -- a-b | |
negate | a -- -a | |
* | a b -- a*b | |
/ | a b -- a/b | |
reciprocal | a -- 1/a | |
mod | a b -- r | |
rem | a b -- r | |
square | a -- a^2 | square: dup * return
|
sqrt | a -- s | square root |
exponent | b x -- b^x | |
is-integer | a -- flag | |
floor | a -- n | Rounds down. |
ceiling | a -- n | Rounds up. |
round | a -- n | Rounds to nearest. |
min | a b -- a|b | Returns the lesser (closer to negative infinity) of the arguments. |
max | a b -- a|b | Returns the greater (closer to positive infinity) of the arguments. |
abs | a -- b | b = |a| |
signum | a -- b | b = 1 if a positive, -1 if negative, 0 otherwise. |
reorient | a -- b | Ensures an angle is in (-pi, pi]. |
sin | a -- sina | |
cos | a -- cosa | |
tan | a -- tana | |
arcsin | sina -- a | |
arccos | cosa -- a | |
arctan | tana -- a | |
arctan2 | x y -- a | |
random | min max -- value | Returns a random real value evenly distributed between the two bounds (inclusive). |
random-angle | -- angle | Returns a random value evenly distributed in (-pi, pi]. |
random-int | min max -- value | Returns a random integer evenly distributed between the two bounds (inclusive). The bounds need not be integers, but the result will be; 1.5 2.5 random-int returns 2.
|
random-bool | probability -- boolean | Returns a boolean, which will be 1 with the given probability. |
pi | -- pi | |
2pi | -- 2pi | |
pi/2 | -- pi/2 | |
e | -- e | 2.7182818284 |
epsilon | -- epsilon | Returns the smallest difference representable. |
infinity | -- inf | Returns the largest positive value representable. (Negative infinity is actually infinity negate epsilon - .)
|
Vector operations | ||
rect-to-polar | x y -- magnitude angle | Convert a vector from rectangular to polar form. |
polar-to-rect | magnitude angle -- x y | Convert a vector from polar to rectangular form. |
v+ | x1 y1 x2 y2 -- x1+x2 y1+y2 | Adds two vectors in rectangular form. |
v- | x1 y1 x2 y2 -- x1-x2 y1-y2 | Subtracts two vectors in rectangular form. |
vnegate | x y -- -x -y | Negates a vector. |
vs* | x y s -- x*s y*s | Vector-scalar multiply. |
vs/ | x y s -- x/s y/s | Vector-scalar divide. |
norm | x y -- p | Returns x square y square + sqrt , the Pythagorean sum.
|
angle | x y -- angle | Returns arctan2(y, x). |
dot | x1 y1 x2 y2 -- v | Dot product. v = x1 x2 * y1 y2 * +
|
project | x1 y1 x2 y2 -- x3 y3 | Returns the projection of the first vector onto the second. |
cross | x1 y1 x2 y2 -- v | Two-dimensional cross product, sort of. v = x1 y2 * y1 x2 * - . This is the z-component of the equivalent three-dimensional cross product.
|
unitize | x y -- x' y' | Returns a vector of magnitude 1 in the same direction. |
dist | x1 y1 x2 y2 -- d | Distance (norm of difference) operator. |
in-range | x1 y1 x2 y2 r-- flag | whether the two vectors differ by less than r. |
Comparisons and Boolean operations | ||
= | a b -- flag | |
<> | a b -- flag | |
< | a b -- flag | |
> | a b -- flag | |
<= | a b -- flag | |
>= | a b -- flag | |
not | f1 -- f2 | f2 is zero if f1 was nonzero, and one otherwise. |
and | f1 f2 -- f3 | f3 is one if both f1 and f2 were nonzero, and zero otherwise. |
or | f1 f2 -- f3 | f3 is zero if both f1 and f2 were zero, and one otherwise. |
xor | f1 f2 -- f3 | f3 is one if exactly one of f1 or f2 was non-zero, and zero otherwise. |
nand | f1 f2 -- f3 | f3 is zero if both f1 and f2 were nonzero, and one otherwise. |
nor | f1 f2 -- f3 | f3 is one if both f1 and f2 were zero, and zero otherwise. |
ifev | flag a b -- a|b | Value conditional: returns a if flag was nonzero, and b otherwise. |
Miscellaneous | ||
print | value -- | Remove and display one item, for debugging purposes. |
vprint | x y -- | Remove and display a vector. |
beep | -- | Beep, for debugging purposes. |
pause | -- | Pauses the simulation. For debugging. |
stop | -- | Permanently stops execution of this brain. |
sync | -- | Waits until the next frame to continue execution. Useful for waiting for hardware to work, or for synchronizing to do something all in one frame. |
store | x addr -- | Writes a value to local memory. |
load | addr -- x | Reads local memory. |
vstore | x y addr -- | A convenience, to store two values. |
vload | addr -- x y |
Some words execute at compile time, for convenience in writing common control structures. There are conditionals and loops so far.
Structure | Meaning | Expansion |
---|---|---|
test if body then
| execute body if test is true | test label& nifg body label:
|
test if body1 else body2 then
| execute body1 if test is true, body2 otherwise | test skip& nifg body1 done& jump
|
nif ...
| If, with sense reversed | test label& ifg ...
|
test1 if test2 and-if body1 else body2 then
| if test1 is true, then if test2 is true, then execute body1... | test skip& nifg test2 skip& nifg body1 done& jump
|
test1 if body1 else test2 if body2 celse test3 if body3 celse body4 then
| Makes if-elsif-elsif-else constructs shorter (no need to write then then then ).
| test1 skip1& nifg body1 done& jump
|
do code forever
| infinite loop | repeat: code repeat& jump
|
do test while code loop
| Execute test and code until test is true (until ) or false (while ). Note that the end-test is in the middle of the loop.
| repeat: test done& nifg
|
do test until code loop
| repeat: test done& ifg
| |
do code test while-loop
| optimized versions of while loop and until loop , saving two instructions
| repeat: code test repeat& ifg
|
do code test until-loop
| repeat: code test repeat& nifg
|
Grobots by Devon Schudy (dschudy@yahoo.com) and Warren Schudy (wschudy@wpi.edu)