Advent of Code 2025 in Charly
Day 2
This article is part of my series on implementing each Advent of Code 2025 challenge in my own programming language Charly.
Today’s task involved determining the set of invalid IDs from a list of ID-ranges. An ID was invalid if its string representation consisted of two repeated substrings of equal length.
E.g. 55, 123123, 827827 are
all invalid IDs.
The second part modified the challenge to instead define an invalid
ID as a repetition of N equally-sized substrings.
I solved the problem by looping over each ID in the provided ranges,
determining the sets of potential substrings that could form the entire
ID and then checking if that ID actually consists only of N
repetitions of that substring.
Charly does support concurrent and parallel execution of code, which I attempted at first, but the overhead of that implementation almost doubled the execution time, so I left it at a single-threaded implementation.
#!/usr/local/bin/charly
if ARGV.length < 2 {
print("Missing filepath")
exit(1)
}
const input_path = ARGV[1]
const input = readfile(input_path)
const ranges = input
.split(",")
.map(->(entry) {
const (first, last) = entry.split("-")
(first.to_number(), last.to_number())
})
const allIds = []
ranges.each(->(range) {
const (first, last) = range
first.upTo(last, ->(idx) {
allIds.push(idx)
})
})
func checkIsInvalid(entry) {
const length = entry.length
const possibleParts = 1.collectUpTo(length / 2, ->(partLength) {
entry.substring(0, partLength)
})
.filter(->(part) {
// filter out parts that cannot possibly form the entire pattern
entry.length % part.length == 0
})
possibleParts.filter(->(part) {
const multiplier = entry.length / part.length
const check = part * multiplier
check == entry
}).notEmpty()
}
const invalidIds = allIds
.map(->(id) "{id}")
.filter(->(entry) {
checkIsInvalid(entry)
})
.map(->(entry) entry.to_number())
const totalSum = invalidIds.reduce(0, ->(p, c) p + c)
print(totalSum)Changes to the stdlib / VM
The only additions to the standard library today were the
Int::upTo and List::notEmpty methods. You can
see the implementations of those methods below:
func upTo(other, callback) {
assert other instanceof Number
if other < self {
return []
}
const result = []
let i = self
while i <= other {
const r = callback(i)
result.push(r)
i += 1
}
result
}func notEmpty = @length > 0The individual commits can be found here:
Late-night Addendum
Charly does support concurrent and parallel execution of code, which I attempted at first, but the overhead of that implementation almost doubled the execution time, so I left it at a single-threaded implementation.
This didn’t quite sit right with me, so I investigated a bit. It turned out that that the runtime was heavily bottlenecked by mutex locks/unlocks, caused by the VM accessing the global list of object shape instances and reading to / writing from global variables.
I’ve removed locks where possible, added a per-processor shape
caching layer and also replaced the default
std::unordered_map hashtable used for global variables with
greg7mdp/parallel-hashmap.
These changes, alongside some minor tweaks to the implementation of
the puzzle solving code, brought down execution time by around
60%. Multi-threading the execution also
resulted in a measurable performance improvement, altough not as much as
I would’ve liked.
Below you can see the updated implementation of today’s puzzle:
#!/usr/local/bin/charly
if ARGV.length < 2 {
print("Missing filepath")
exit(1)
}
const input_path = ARGV[1]
const input = readfile(input_path)
const ranges = input
.split(",")
.map(->(entry) {
const (first, last) = entry.split("-")
(first.to_number(), last.to_number())
})
const invalidIdsPerRange = ranges
.parallelMap(->(range) {
const (first, last) = range
const invalidIds = []
first.upTo(last, ->(i) {
const entry = "{i}"
const length = entry.length
const parts = 1.collectUpTo(length / 2, ->(partlength) entry.substring(0, partlength))
const possibleParts = parts.filter(->(part) {
entry.length % part.length == 0
})
const isInvalid = possibleParts.any(->(part) {
const multiplier = entry.length / part.length
const check = part * multiplier
check == entry
})
if isInvalid {
invalidIds.push(i)
}
})
invalidIds
})
.flatten()
const totalSum = invalidIdsPerRange.reduce(0, ->(p, c) p + c)
print(totalSum)And all the commits that made up tonights late-night hacking session:
29bc22aImplemented per-proc shape lookup cached752e6eUse parallel_hashmap for global variables5c2c36aAdd List::anyc7ffd2eAdd List::flatten3a06101Add List::parallelMap and Int::parallelMap387dea6Add Int::parallelTimes2059053Add Int::collectUpTo
Even later late-night Addendum
I had to revert some previous changes, because my previous commits caused some crazy unpredictable segfaults to happen. While I don’t believe the locking I added back in again truly solved the issue, they’re merely masking it, I do kind of need a semi-working language to solve the upcoming puzzles.
I shall revisit this at a later time.
908f02cAdded locks around builtin class logic again
Links
Copyright © 2024 - 2025 Leonard Schütz | Attributions This post was written by a human and reviewed and proof-read by LLMs