Advent of Code 2025

Posted on Sat 27 December 2025 in tech

I got 1 ⭐️ this year.

I am not committing to the Advent of Code 2025 this year. I'm working on too many side projects: checksmix, snipren, emomtimer. In fact I don't even have time to write this blog. On the other hand, I used some holiday downtime to work through Day 1 Part 1 in MMIX.

Day 1 is about a safe dial with 100 increments. The dial starts at 50, then processes a stream of rotations such as L68 or R48. Each instruction moves the dial left or right by the given amount, wrapping modulo 100. The trick is that, while mod addition is no big deal, it will be obviously positive, modulo subtraction is a bit more complicated because the pointer could take a negative value.

RemEuclid DIV     $2,$0,$1              # floor quotient, remainder to rR
          GET     $0,rR                 # in [0,m) when m > 0
          POP     1,0

Implementation

  • Parse the input as a null-terminated string containing L/R tokens separated by newlines.
  • Convert each token into a signed direction (-1 for L, +1 for R) and a magnitude.
  • Update the dial position by adding the signed delta and applying Euclidean remainder with divisor 100 to keep the value in [0, 99].
  • Record a hit whenever the dial value becomes zero.

InputPtr is a global register used as a moving pointer into the input string. Registers that should survive a PUSHJ/POP call boundary, Dial, OpCount, HitCount, Direction, Magnitude, are declared GREG. A value left in a register is not returned from a subroutine unless it is a global.

ParseRotations reads one token and returns the direction in Direction and the magnitude in Magnitude. HandleRotation computes the signed move, updates the dial in Dial, wraps it with RemEuclid, and increments the hit counter HitCount. The loop continues until a null terminator is encountered.

RemEuclid is a small helper that mirrors Rust's rem_euclid to avoid negative results after subtraction. Signed DIV floors and the helper reads the remainder from rR. The quotient is in $2.

Complexity is linear in the number of tokens, which matters because Advent of Code gives you a big corpus to check that your performance is not exponential.

Input and code are embedded together below. The input matches the sample I used to validate the counting logic.

DIAL_INCREMENTS IS 100

          LOC    #100
          GREG   @
InputPtr  GREG   0                      # pointer into the input string
Dial      GREG   0                      # dial position
OpCount   GREG   0                      # operation count
HitCount  GREG   0                      # zero-hit count
Direction GREG   0                      # parsed direction
Magnitude GREG   0                      # parsed magnitude

# Entry point
Main    LDA     InputPtr,PuzzleInput    # initialize pointer
        SET     Dial,50                 # dial setting
        SET     OpCount,0               # count number of operations
        SET     HitCount,0              # hit count - how many times the dial got set to 0

TokenLoop
        LDB     $1,InputPtr,0           # check if at end BEFORE parsing
        BZ      $1,Done                 # null terminator, done
        PUSHJ   $0,ParseRotations
        PUSHJ   $0,HandleRotation
        ADDU    OpCount,OpCount,1       # increment operations processed
        JMP     TokenLoop


# ----------------------------------------------------
# HandleRotation
# ----------------------------------------------------
HandleRotation
        GET     $0,rJ                   # save return address across the nested PUSHJ
        MUL     $5,Direction,Magnitude  # operation = direction * magnitude
        ADD     $2,Dial,$5              # arg0 = dial + operation
        SET     $3,DIAL_INCREMENTS      # arg1 = divisor
        PUSHJ   $1,RemEuclid            # $0 stays hidden, result lands at $1
        SET     Dial,$1
        PUT     rJ,$0                   # restore return address
        BNZ     Dial,SkipCount
        ADD     HitCount,HitCount,1     # dial is zero: count a hit
SkipCount
        POP     0,0


# ------------------------------------------------------------
# RemEuclid: $0 := ($0 rem_euclid $1), with $1 > 0
# Input: $0 = dividend, $1 = divisor (must be > 0)
# Output: $0 = remainder in range [0, $1)
# Uses: $2
# ------------------------------------------------------------
RemEuclid DIV     $2,$0,$1              # floor quotient, remainder to rR
          GET     $0,rR                 # in [0,m) when m > 0
          POP     1,0

# ----------------------------------------------------
# ParseRotations - Parse ONE rotation from input string
# Input: InputPtr = pointer to current position in string (global)
# Returns: Direction = -1 or +1, Magnitude = value (both GREGs), InputPtr = updated pointer
# Uses: $1 = current char, $2 = comparison scratch, $5/$6 = digit scratch
ParseRotations
        LDB     $1,InputPtr,0           # load first char
        BZ      $1,ParseEnd             # null terminator
        CMP     $2,$1,'L'               # check if 'L'
        BZ      $2,ParseL
        CMP     $2,$1,'R'
        BZ      $2,ParseR
        ADDU    InputPtr,InputPtr,1     # skip unknown char
        POP     0,0                     # return early

ParseL  NEG     Direction,0,1           # direction = -1 for left
        JMP     ParseNumber
ParseR  SET     Direction,1             # direction = +1 for right

ParseNumber
        ADDU    InputPtr,InputPtr,1     # skip L/R char
        SET     Magnitude,0             # value accumulator
DigitLoop
        LDB     $1,InputPtr,0           # load next char
        SUB     $5,$1,'0'               # convert to digit
        BN      $5,EndNumber            # < '0'
        CMP     $6,$5,10
        BNN     $6,EndNumber            # >= 10
        MUL     Magnitude,Magnitude,10  # value *= 10
        ADD     Magnitude,Magnitude,$5  # value += digit
        ADDU    InputPtr,InputPtr,1
        JMP     DigitLoop

EndNumber
        ADDU    InputPtr,InputPtr,1     # skip newline/delimiter

ParseEnd
        POP     0,0

Done    TRAP    0,Halt,0

        LOC     Data_Segment

PuzzleInput BYTE    "L68", '\n', "L30", '\n', "R48", '\n'
            BYTE    "L5", '\n', "R60", '\n'
            BYTE    "L55", '\n', "L1", '\n', "L99", '\n'
            BYTE    "R14", '\n', "L82", '\n', 0

This program is using remark-style comments that are documented in The Art of Computer Programming, Volume 1, Fascicle 1, Section 1.3.2´. An MMIX statement is made up of a LABEL, OP and EXPR. Everything to the right of EXPR is considered a comment, unless it starts with a digit or an operator. This has the funny consequence that the # is entirely irrelevant and used here to orient the eye.

For this input the correct answer is 3. The dial landed on zero 3 times:

Rotation Dial
start 50
L68 82
L30 52
R48 0
L5 95
R60 55
L55 0
L1 99
L99 0
R14 14
L82 32