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/Rtokens separated by newlines. - Convert each token into a signed direction (
-1forL,+1forR) 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 |