GRUBBY

From Esolang
Jump to navigation Jump to search

GRUBBY is a name given to an underutilized graphical "programming language" by User:BoundedBeans.

Description

To run a GRUBBY program, start up some sort of x86 virtual machine booting off of some sort of medium or disk containing only the GRUB2 bootloader and a single grub.cfg file containing the text of the program. Note that since the initial GRUB must be on the EFI partition, which uses a filesystem similar to FAT32, there is a limit of around 4 gigabytes of program text, since that is the maximum size of a file in FAT32. This limit does not necessarily prevent GRUBBY from being Turing-complete, since a Turing-complete system may be constructable within 4 gigabytes using disk or RAM storage (disk space and RAM size is not usually considered to affect computational class as it is a technical limitation that does not affect the language).

The full language specification is spread across the Official GNU GRUB2 Manual.

Tricks for computation

The first major limitation is that there is no numerical arithmetic. This is tricky and slow to implement, but is just barely possible with the features GRUB2 configuration files provide.

Data storage

You can store in environment variables using the set commands.

Conditionals and looping

The first thing we need are conditionals and looping.

Luckily we have for, if, elif, else, while, similar to the Bourne shell commands of the same name. We also have until, which is redundant with while with an inverted condition but makes things slightly easier, and function, which is really good since we can organize the code.

$? is the exit status of the previous command, which is useful if we want to store it in a variable to check later, since otherwise commands would have to be directly in a conditional.

The loops, however, are constant length (using for with n words) until we can make a given length of number (such as strings of 16 or 32 binary digits).

Lookup table

We can create a binary lookup table for logic gates operating on strings containing one binary digit, using conditionals.

Strings of digits

We can create a full number with a list of bits, but to to make an adder or subtractor using the logic gate lookup tables, we need ways to concatenate strings and separate them.

Luckily, concatenation is easy. Just store both segments in variables s1 and s2, then store ${s1}${s2} in a variable. (You can change s1 and s2 to any valid identifier).

Separating strings is a lot harder. The only way the author could find is the regexp command. This command has an argument --set [name] to assign the match to a variable, but you can prefix the name with a number n and a colon to store the nth match of the regex in the string.

So, to get the first character of the variable string, use:

regexp --set result '.' ${string}

Every other character except the first can be obtained with:

regexp --set 2:result '^.|.*$' "${string}"

The result will be stored in the variable result.

This means numbers are optimally stored with the least significant bit first, in order to be added and subtracted.

Indefinite loops

Once we make numbers, we can now use them for looping. We can reverse a least-significant first binary string by taking the head over and over, replacing the string with its tail, and prepending the head to a new growing string. The reversed string will lexicographically order itself in the same way than numbers would order themselves, though negative numbers would complicate this and maybe require an additional conditional.

Using the test command allows us to check string equality and lexicographical comparison with the ==, !=, <, >, <=, and >= operators. By using a fixed length number, we can have arbitrary counted for loops, as well as richer while and until loops.

I/O

Printing to the screen can be done with the echo command.

Input can be done with the read command, which has the option of storing into a variable.

Drivers

Drivers can be implemented with the undocumented in* and out* commands for reading and writing I/O ports. From here you can connect to the disk, keyboard, mouse, USB devices, network, etc. This is just enough to implement a full operating system in GRUBBY, along with the also undocumented read_* and write_* for RAM and memory-mapped devices.

Seemingly, through reading the source code (grub-core/commands/iorw.c and grub-core/commands/memrw.c), the formats are:

in(TYPE) [-v VARNAME] PORT
out(TYPE) PORT VALUE [MASK]
read_(TYPE) [-v VARNAME] ADDR
write_(TYPE) ADDR VALUE [MASK]

Arbitrary assembly execution

There appears to be no way to do this in RAM, since the program counter is a register, not a RAM value.

The only way to do this is:

  1. Write a disk driver for every disk that you plan on being compatible with, using the information in the #Drivers section.
  2. Use the appropriate driver to write the code onto the disk.
  3. Use the chainloader command to boot into another bootloader and maybe kernel on disk. Potentially an entire operating system, though the process of writing it and everything else would have to fit into 4 gigabytes, putting a harsh limit on the size of the operating system.

Unfortunately, the chainloader command can only be used from within a menuentry block. The good thing is that menuentry supports any commands inside of its block, so we can just write all the code inside of a single menuentry, then execute it automatically by setting the timeout variable outside of the menuentry block (setting it to 0 runs it instantly).

Self-modification

It's possible using drivers for a GRUB configuration file to modify its own file on disk. I'm not really sure how that would play out since it depends on whether GRUB reads it all at once (in which case it would require a restart), or sequentially (in which case self-modification could happen at runtime but would have weird effects).

GRUB configurations files are also capable of loading other configuration files of the same format using source FILE, and that configuration file could be rewritten and executed again for more nuanced eval. Potentially it could also load itself???, in which case self-modification may be somewhat simpler.

Examples

If the command line doesn't show up for these, try pressing c.

Hello, world!

echo 'Hello, world!'

Truth-machine

read truth
if test truth == 0;
then
  echo 0
elif test truth == 1;
  while true;
  do
    echo 1
  done
fi

Logic gate calculator

Input as (1|0) (AND|OR|XOR) (1|0).

function AND {
  if test $1 == 0;
  then
    set result=0
  else
    set result=$2
  fi
}

function OR {
  if test $1 == 1;
  then
    set result=1
  else
    set result=$2
  fi
}

function XOR {
  if test $1 == $2;
  then
    set result=0
  else
    set result=1
  fi
}

read logic
regexp --set gate 'AND|X?OR'
regexp --set 1:left '[10]'
regexp --set 2:right '[10]'
eval ${gate} ${left} ${right}
echo ${result}

Truth-machine with a nice menu for selecting 1 or 0

menuentry "0/False" {
  echo 0
}

menuentry "1/True" {
  while true;
  do
    echo 1
  done
}