Advent of Code 2025

Posted on Sat 27 December 2025 in update

I got my ⭐️...

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 idea is that directional, left or right, rotations are applied to the dial pointer using mod math. The trick is that, while mod addition is no big deal, it will be obviously positive, modullo subtraction is a bit more complicated because the pointer could take a negative value. In that case the pointer needs to be reset from the back (end - pointer) to determine the pointed value.

Consider the following:

RemEuclid  DIV     $2, $0, $1        ; q = trunc(a/m)
           MUL     $3, $2, $1        ; q*m
           SUB     $0, $0, $3        ; r = a - q*m  (can be negative)
           BNN     $0, RemDone       ; if r >= 0, we're done
           ADD     $0, $0, $1        ; r += m
RemDone    POP     1,0

Note that after the branch, BNN, instruction a correction is applied to add the remainder to the max number of increments, in this case 100 to determine the correct offset, aka, Euclidean remainder.

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. State that has to survive a PUSHJ/POP call boundary — Dial, OpCount, HitCount, Direction, Magnitude — is declared GREG alongside it; MMIX's register stack renumbers plain local registers per call frame, so a value left in a bare $N inside a callee never makes it back to the caller unless it's either a true global or explicitly staged at the call's hole register. 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 when the dial reaches zero. 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. The helper keeps the remainder non-negative by adding the divisor when needed; it uses $2 and $3 as scratch registers.

Complexity is linear in the number of tokens. The program keeps all state in registers and a single input buffer in the data segment; no heap or I/O is involved beyond the initial static input.

Input and code are embedded together below. The input matches the sample I used to validate the counting logic: the dial hits zero whenever a rotation lands exactly on that position after wrapping.

DIAL_INCREMENTS IS 100

        LOC #100
; Entry point
        GREG    @
InputPtr  GREG   0               ; pointer into the input string
Dial      GREG   0               ; dial position - persists across calls
OpCount   GREG   0               ; operation count - persists across calls
HitCount  GREG   0               ; zero-hit count - persists across calls
Direction GREG   0               ; parsed direction, threaded ParseRotations -> HandleRotation
Magnitude GREG   0               ; parsed magnitude, threaded ParseRotations -> HandleRotation

Main    LDA    InputPtr,PuzzleInput   ; initialize pointer
        SETI    Dial,50         ; dial setting - current dial pointer (starts at 50)
        SETI    OpCount,0       ; count number of operations handled
        SETI    HitCount,0      ; hit count - how many times the dial got set to 0

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


; ----------------------------------------------------
; HandleRotation
; ----------------------------------------------------
HandleRotation
        ; operation = direction * magnitude
        MUL     $5,Direction,Magnitude  ; apply direction to value
        ; arg0 = dial + operation, staged at $1 for PUSHJ $0
        ADD     $1,Dial,$5      ; move dial in direction of vector
        ; arg1 = DIAL_INCREMENTS, staged at $2
        SETI    $2,DIAL_INCREMENTS
        PUSHJ   $0,RemEuclid    ; result lands at hole == $0
        ; dial = temp
        SET     Dial,$0
        ; if dial == 0?
        BNZ     Dial,SkipCount
        ; if dial is zero increment the number of hits
        ADDI     HitCount,HitCount,1
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, $3
; ------------------------------------------------------------
RemEuclid  DIV     $2, $0, $1        ; q = trunc(a/m)
           MUL     $3, $2, $1        ; q*m
           SUB     $0, $0, $3        ; r = a - q*m  (can be negative)
           BNN     $0, RemDone       ; if r >= 0, we're done
           ADD     $0, $0, $1        ; r += m
RemDone    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
        LDBI    $1,InputPtr,0   ; load first char
        BZ      $1,ParseEnd     ; null terminator
        CMPI    $2,$1,'L'       ; check if 'L'
        BZ      $2,ParseL
        CMPI    $2,$1,'R'
        BZ      $2,ParseR
        ADDUI   InputPtr,InputPtr,1  ; skip unknown char
        POP     0,0             ; return early

ParseL  SETI    Direction,-1    ; direction = -1 for left
        JMP     ParseNumber
ParseR  SETI    Direction,1     ; direction = +1 for right

ParseNumber
        ADDUI   InputPtr,InputPtr,1  ; skip L/R char
        SETI    Magnitude,0     ; value accumulator
DigitLoop
        LDBI    $1,InputPtr,0   ; load next char
        SUBI    $5,$1,'0'       ; convert to digit
        BN      $5,EndNumber    ; < '0'
        CMPI    $6,$5,10
        BNN     $6,EndNumber    ; >= 10
        MULI    Magnitude,Magnitude,10  ; value *= 10
        ADD     Magnitude,Magnitude,$5  ; value += digit
        ADDUI   InputPtr,InputPtr,1
        JMP     DigitLoop

EndNumber
        ADDUI   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

The example input the program processes 10 operations, finishes at dial position 32, and records 3 zero hits in HitCount.