uku.dev

Advent of Languages: Day 5 - Bash

11 December, 2019

2 minute read

This is a part of my ongoing challenge of solving every Advent of Code puzzle in a different language.

The challenge we're tackling today is available here.

Sunny with a Chance of Asteroids

This task is about making additions to the IntCode machine we built on day 2. This means we have to start by rebuilding our existing IntCode implementation in the new language, and then build this task.

The Language

For no particular reason, I picked bash. Bash is a command shell and language, being the default shell in Unix machines. It’s great for tasks that amount to gluing command-line tools together, which means mastering it can make it easier to automate your work. However, it’s filled with gotchas and not meant to do actual computational work. Let’s do some actual computational work in it.

For the record, I am using bash 5 here.

The Solution: Part 1

As usual, let’s start with the code.

#!/usr/bin/env bash
input_file="$1"

set -ue

# program codes
ADD=1
MULTIPLY=2
INPUT=3
OUTPUT=4
HALT=99

# program modes
POSITION=0
IMMEDIATE=1

if [ "$input_file" == "" ]; then
    echo "NO PROGRAM FILE PROVIDED"
    exit 1
fi

IFS=',' read -r -a tape <<< $(cat $input_file)

inputs=(1)

function run_intmachine() {
    local index=0

    while [ true ]
    do
        local tape_value=${tape[$index]}
        local operation=$(($tape_value%100))
        local parameter_modes=$(($tape_value/100))

        case "$operation"
        in
            "$ADD")
                local arg_1=${tape[$index + 1]}
                local arg_1_mode=$((parameter_modes % 10))
                local arg_1_val=$([ "$arg_1_mode" -eq $IMMEDIATE ] && echo "$arg_1" || echo "${tape[$arg_1]}")

                local arg_2=${tape[$index + 2]}
                local arg_2_mode=$((parameter_modes / 10 % 10))
                local arg_2_val=$([ "$arg_2_mode" -eq $IMMEDIATE ] && echo "$arg_2" || echo "${tape[$arg_2]}")
                local value_position=${tape[$index + 3]} # Will never be in immediate mode.

                local sum=$(($arg_1_val + $arg_2_val))
                tape[$value_position]=$sum

                index=$(($index + 4))
            ;;
            "$MULTIPLY")
                local arg_1=${tape[$index + 1]}
                local arg_1_mode=$((parameter_modes % 10))
                local arg_1_val=$([ "$arg_1_mode" -eq $IMMEDIATE ] && echo "$arg_1" || echo "${tape[$arg_1]}")

                local arg_2=${tape[$index + 2]}
                local arg_2_mode=$((parameter_modes / 10 % 10))
                local arg_2_val=$([ "$arg_2_mode" -eq $IMMEDIATE ] && echo "$arg_2" || echo "${tape[$arg_2]}")
                local value_position=${tape[$index + 3]} # Will never be in immediate mode.

                local result=$(($arg_1_val * $arg_2_val))
                tape[$value_position]=$result

                index=$(($index + 4))
            ;;
            "$INPUT")
                local arg_1=${tape[$index + 1]} # Will never be in immediate mode.

                tape[$arg_1]=${inputs[0]}
                inputs=(${inputs:1}) # remove an element

                index=$(($index + 2))
            ;;
            "$OUTPUT")
                local arg_1=${tape[$index + 1]}
                local arg_1_mode=$((parameter_modes % 10))
                local arg_1_val=$([ "$arg_1_mode" -eq $IMMEDIATE ] && echo "$arg_1" || echo "${tape[$arg_1]}")

                printf "$arg_1_val\n"

                index=$(($index + 2))
            ;;
            "$HALT")
                printf "PROGRAM HALTED\n"
                break
            ;;
            *)
                printf "UNKNOWN OPERATION $operation\n"
                exit 1
            ;;
        esac
    done
}

run_intmachine

In the first part of the solution, we need to do 3 things:

  1. Re-implement the original implementation in bash
  2. Add support for input and output commands, where input just reads from some list of inputs
  3. Add support for parameter modes

In this solution, we start with some boilerplate. First, the hashbang (#!/usr/bin/env bash) denotes that this file can be run using bash. Then, we define the global variable input_file to be the first argument passed to our bash program from the command line. Then, we set bash flags so it would exit when a command fails or when something is undefined.

After this, we get to us defining a bunch of constants for the operations and modes we’ll use later. Then we check if we were actually passed a filename to run and if not, we exit immediately. Following this, we read the contents of the file into an array with an unwieldy command.

Then we get to the solution. We define our inputs array and the function that runs our machine (which we immediately execute). The machine works identically to our SCSS solution but gets more complicated with the addition of parameter modes. We essentially have to add support for any command to define whether its arguments are pointers to values on the tape, or if they’re directly the values themselves. These modes are stored in the command code number and we retrieve them with division and modulo operations. It turned out quite unwieldy, as bash’s abstractions aren’t the best for this sort of thing.

The Solution: Part 2

In part two we have to implement some new commands for comparison and jumping to locations based on the comparison’s result. It’ pretty straightforward. I won’t include the code here as it’s quite long, but you can check it out on GitHub.

Retro

Bash was not very nice to use for this, unsurprisingly. However, the task itself was pretty fun. We’re building a cpu here, essentially. It’s cool how just a couple of days ago we were explaining operations and systems similar to this when we were using assembly, and now we’re implementing them. Perhaps I’ll do a later task in IntCode if I’m feeling particularly masochistic.

If you want to look at the code in full, I’m putting all of the Advent of Code solutions on GitHub.