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.
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 DIGIT
s. 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 F
ails. 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
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 S
ucceeds. 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