Register

Advent of Code 2024

kyle

For today, I'll be using SNOBOL, a powerful and charmingly old-fashioned language from the 60s that was commonly taught to non-CS people for a couple decades. It has first-class support for pattern-matching, similar to regexes, but more powerful in some ways (like allowing recursion) and less convenient in others. Control-flow is done with gotos, but you can make a goto conditional on whether a pattern on the same line matched or not. Every line of SNOBOL code is of the form "label subject pattern = object : transfer", where every one of those non-symbol elements is optional (the = is required if there's an object, and the : is required if there's a transfer).

I decided to do today's puzzles in SNOBOL before I looked at the problems, but SNOBOL ended up being a very good fit.

Part 1

The first task is to find every mul(x,y) term in a file and compute the sum of all these products. Each x/y can be 1-3 digits. In regex, it's /mul\(\d{1,3},\d{1,3}\)/g.

To solve this in SNOBOL, we can begin by defining some patterns. The first, DIGIT, will match any single digit. Number will match one, two, or three DIGITs. Finally, MUL will match a complete mul(x,y) form. In MUL, we use the . operator to assign the two Number matches into X and Y respectively. Finally, we also initialise a SUM variable to accumulate our result:

        DIGIT  = ANY('0123456789')
        NUMBER = DIGIT | DIGIT DIGIT | DIGIT DIGIT DIGIT
        MUL    = 'mul(' NUMBER . X ',' NUMBER . Y ')'

        SUM = 0

Now we begin our line-reading loop with the label NEXTL. LINE = INPUT reads a line from the standard input and stores it in LINE. :F(DONE) jumps to DONE if the match Fails. Otherwise, control continues to the next line.

NEXTL LINE = INPUT :F(DONE)

Note that this line has everything except a pattern: a label, subject, object, and transfer.

After successfully reading a line, we place another label NEXTW for the next "word" that we care about, and then we do LINE ARB MUL = :F(NEXTL). The subject of this line is LINE, and we match it against the pattern ARB MUL. ARB matches any number of anything lazily, like /.*?/ in regex, and MUL is our term pattern from before. We can replace the part of a variable that matched a pattern by assigning to it. Here, we want to delete the matching part so that we're consuming LINE and it will start after this match when we go for the next word, so we put nothing after the = (and before the :). If the pattern match fails, we process the next line of input (because there are no more mul forms in this line) with :F(NEXTL), otherwise we just continue by executing the next statement, which adds the prodoct into SUM and then unconditionally jumps to NEXTW:

NEXTW   LINE ARB MUL =                           :F(NEXTL)
        SUM          = SUM + X * Y               :(NEXTW)

After that, we just need a DONE label that prints the result, and then an END label that ends the program. To print something, you assign it to OUTPUT, so we want OUTPUT = SUM. Here's the whole thing:

        DIGIT  = ANY('0123456789')
        NUMBER = DIGIT | DIGIT DIGIT | DIGIT DIGIT DIGIT
        MUL    = 'mul(' NUMBER . X ',' NUMBER . Y ')'

        SUM = 0

NEXTL   LINE         = INPUT                     :F(DONE)
NEXTW   LINE ARB MUL =                           :F(NEXTL)
        SUM          = SUM + X * Y               :(NEXTW)

DONE    OUTPUT       = SUM

END

Part 2

For the second puzzle, we need to enhance our script to support do() and don't() terms which enable and disable the summing of products from mul terms; any mul terms after a don't() and before a do() are just ignored.

This program is a little more substantial, but uses essentially the same techniques. The only new thing we need is a transfer like S(foo), which jumps to foo if the match Succeeds. Aside from that, we just add patterns for do() and don't(), introduce a new OP pattern that matches any of the terms we care about, and use a COUNTP flag to track whether we should be accumulating results:

        DIGIT   = ANY('0123456789')
        NUMBER  = DIGIT | DIGIT DIGIT | DIGIT DIGIT DIGIT
        MUL     = 'mul(' NUMBER . X ',' NUMBER . Y ')'
        ENABLE  = 'do()'
        DISABLE = "don't()"
        OP      = MUL | ENABLE | DISABLE

        COUNTP = 'YES'
        SUM    = 0

NEXTL   LINE               = INPUT               :F(DONE)
NEXTW   LINE ARB OP . TERM =                     :F(NEXTL)
        TERM ENABLE                              :S(ENABLE)
        TERM DISABLE                             :S(DISABLE)
        COUNTP 'YES'                             :F(NEXTW)
        SUM                = SUM + X * Y         :(NEXTW)
ENABLE  COUNTP             = 'YES'               :(NEXTW)
DISABLE COUNTP             =                     :(NEXTW)

DONE    OUTPUT             = SUM

END
  • programming
First posted 12/3/2024, 12:32:44 AM
Updated 12/30/2024, 8:51:16 PM