So What Is A Binary Heap?
A binary heap is really just an sorted array pretending to be a tree. Each element has a parent and children, but instead of pointers you find them with simple array indexes. The trick is that every parent follows one rule relative to its children, and that rule decides what kind of heap you get:
- Min heap - every parent is less than or equal to its children. The smallest element always lives at the root/top.
- Max heap - every parent is greater than or equal to its children. The largest element always lives at the root/top.
Because the tree is complete, the parent of element at index i sits at index floor((i−1)/2), and its children sit at 2i+1 and 2i+2. No pointers, no linked lists — just arithmetic on array indices. That cache friendly layout is one reason heaps are so fast in practice.
The key operations:
| Operation | Time | What it does |
|---|---|---|
| push | O(log n) | Insert a value, then sift it up to restore order |
| pop | O(log n) | Remove the root, move the last element up, sift down |
| peek | O(1) | Read the root without removing it |
| make_heap | O(n) | Turn an existing unsorted array into a valid heap (make_heap_min / make_heap_max, new('min'), new('max')) |
This makes a heap the natural backbone of a priority queue — a collection where you always want the next most important item and you need insertions to stay fast.
Why Heap::PQ?
Heap::PQ brings binary heaps to Perl as a C extension. Where a pure Perl heap tops out around 300 operations per second, Heap::PQ's functional interface clocks over 11,000 when pushing a 1000 random integers — and if you are just handling plain integers then there is an optimised NV heap implementation that squeezes out 50% more performance. It ships with three interfaces (object oriented, functional custom ops or a raw array) so you can trade readability for throughput depending on what your code needs.
Installation
cpanm Heap::PQ
Getting Started
Create a heap, push values in, and they come back out in priority order:
use Heap::PQ;
my $pq = Heap::PQ::new('min');
$pq->push(5);
$pq->push(1);
$pq->push(3);
print $pq->peek, "\n"; # 1 — the smallest value is always at the root
while (!$pq->is_empty) {
print $pq->pop, "\n"; # 1, 3, 5
}
Switch to 'max' and the largest element comes out first instead.
Three Ways to Use It
OO Interface
The object oriented API is the clearest to read if you're a traditional perl developer that likes OO. push returns the heap so calls can be chained:
my $heap = Heap::PQ::new('min');
$heap->push(10)->push(20)->push(5);
$heap->push_all(8, 2, 15);
my $top = $heap->pop; # 2
my $size = $heap->size; # 5
Functional Interface
Import with 'import' and Heap::PQ installs heap_push, heap_pop, heap_peek, heap_size, and heap_is_empty as custom ops. Perl compiles them down to optimised opcodes so there is no method dispatch overhead at runtime:
use Heap::PQ 'import';
my $h = Heap::PQ::new('min');
heap_push($h, 42);
heap_push($h, 7);
my $val = heap_pop($h); # 7
my $size = heap_size($h); # 1
my $top = heap_peek($h); # 42
This is the fastest path as stated functional custom ops removes the method->dispatch overhead.
Raw Array Interface
You can also operate directly on a Perl array. Import with 'raw' to get make_heap_min, push_heap_min, pop_heap_min (and their _max counterparts). This skips the object entirely but the functional interface is still faster thanks to custom op optimisation:
use Heap::PQ 'raw';
my @data = (9, 4, 7, 1, 3);
Heap::PQ::make_heap_min(\@data); # O(n) heapify in place
my $min = Heap::PQ::pop_heap_min(\@data); # 1
Heap::PQ::push_heap_min(\@data, 2);
No object, no opaque struct — just an array whose layout satisfies the heap property. Useful when you already have data in an array and want to avoid copying it.
Custom Comparators
By default the heap compares values numerically. Pass a code reference as a second argument to new and you can heap order anything — hash references, objects, strings:
my $tasks = Heap::PQ::new('min', sub {
$a->{due} <=> $b->{due}
});
$tasks->push({ name => 'Write tests', due => 3 });
$tasks->push({ name => 'Ship release', due => 5 });
$tasks->push({ name => 'Fix bug', due => 1 });
while (!$tasks->is_empty) {
my $t = $tasks->pop;
print "$t->{name}\n";
}
# Fix bug
# Write tests
# Ship release
The comparator receives two elements in $a and $b — exactly like Perl's sort — and should return a negative, zero, or positive value. String ordering works the same way:
my $alpha = Heap::PQ::new('min', sub { $a cmp $b });
$alpha->push_all('banana', 'apple', 'cherry');
print $alpha->pop, "\n"; # apple
Key Path Comparators
When your heap contains hash references and you just want to compare by a numeric field, pass the field name as a string instead of a code reference. The comparison happens entirely in C — no Perl callback overhead at all:
my $tasks = Heap::PQ::new('min', 'priority');
$tasks->push({ name => 'Write tests', priority => 3 });
$tasks->push({ name => 'Ship release', priority => 5 });
$tasks->push({ name => 'Fix bug', priority => 1 });
while (!$tasks->is_empty) {
my $t = $tasks->pop;
print "$t->{name}\n";
}
# Fix bug
# Write tests
# Ship release
Dot separated paths reach into nested hashes:
my $heap = Heap::PQ::new('min', 'meta.score');
$heap->push({ id => 'a', meta => { score => 42 } });
$heap->push({ id => 'b', meta => { score => 17 } });
print $heap->pop->{id}; # 'b'
The key is extracted once when you push, so sift operations run at the same speed as a plain numeric heap. In benchmarks, key path heaps match the no-comparator path (~6,400/s) while custom Perl comparators top out around ~1,200/s — a 5× difference.
NV Heaps — Native Doubles, No SV Overhead
When every value is a number, new_nv stores them as raw C doubles instead of Perl scalars. That eliminates SV allocation and reference counting entirely:
my $h = Heap::PQ::new_nv('min');
$h->push_all(3.14, 2.71, 1.41, 1.73);
print $h->pop, "\n"; # 1.41
In benchmarks the NV heap runs roughly 1.5× faster than the functional interface for push heavy workloads, and nearly 57× faster than pure Perl.
Searching and Deleting
Both search and delete accept a callback that receives each element in $_. search returns matching elements; delete removes them and returns how many were dropped:
my $pq = Heap::PQ::new('min');
$pq->push_all(1, 5, 10, 15, 20, 25);
# Find all elements greater than 12
my @big = $pq->search(sub { $_ > 12 });
# @big = (15, 20, 25)
# Remove them from the heap
my $removed = $pq->delete(sub { $_ > 12 });
# $removed = 3, heap now contains (1, 5, 10)
After a delete, Heap::PQ rebuilds the heap in O(n) with Floyd's algorithm so the ordering invariant is always intact.
Peeking at the Top N
peek_n returns the top N elements in sorted order without removing them:
my $scores = Heap::PQ::new('max');
$scores->push_all(88, 95, 72, 99, 84, 91);
my @top3 = $scores->peek_n(3); # (99, 95, 91)
# Heap is unchanged — still has all 6 elements
peek_n works with the heap's built-in numeric comparison, so it is best suited to plain numeric heaps. For heaps that use a custom comparator, pop and re-push to inspect the top N.
Putting It Together: An Event Scheduler
A priority queue is a natural fit for an event loop — push events with a timestamp, pop them in chronological order:
use Heap::PQ 'import';
my $scheduler = Heap::PQ::new('min', 'time'); # compare by the 'time' key in C
heap_push($scheduler, { time => 1712500800, action => 'start_server' });
heap_push($scheduler, { time => 1712497200, action => 'load_config' });
heap_push($scheduler, { time => 1712504400, action => 'open_connections' });
heap_push($scheduler, { time => 1712502600, action => 'warm_cache' });
while (!heap_is_empty($scheduler)) {
my $event = heap_pop($scheduler);
printf "t=%d %s\n", $event->{time}, $event->{action};
}
# t=1712497200 load_config
# t=1712500800 start_server
# t=1712502600 warm_cache
# t=1712504400 open_connections
Benchmarks
All numbers below are from pushing 1,000 random integers then popping them all, measured with Benchmark:
| Implementation | Rate | vs Pure Perl |
|---|---|---|
| Pure Perl | 305/s | — |
| Custom comparator | 1,194/s | 4x |
| Array::Heap | 6,087/s | 20x |
| Heap::PQ OO key path | 6,359/s | 21x |
| Heap::PQ OO | 6,685/s | 22x |
| Heap::PQ raw | 8,665/s | 28x |
| Heap::PQ func | 11,416/s | 37x |
| Heap::PQ NV | 16,747/s | 55x |
The custom op overhead reduction is clearly visible in the functional row, and the NV heap's custom op implementation plus avoidance of SV allocation pushes throughput higher still.
If you have any questions please do comment.
A Side note: I'm currently on look out for a new contract or permanent opportunity. If you know of any relevant openings, I'd appreciate hearing from you - email@lnation.org
TL;DR
Searching for CLI modules on MetaCPAN returns 1690 results. And still I wrote another, Yet Another CLI framework. This is about why mine is the one you want to use.
Introduction
CLI clients, we all write them, we all want them, but the boilerplate is just horrible. I wanted to get rid of it: Burn it with 🔥.
At my previous dayjob we had something I wrote, or co-wrote, my boss wanted a
sort of chef like API. It became zsknife, but had a huge downside. You needed
to register each command in the main module and I didn’t like that at all. I
forked the concept internally, made it so you didn’t needed to register, but it
lacked discovery.
We were all present.
- We noted that core team membership votes are concluding on this day.
- We are waiting for voting to conclude before we kick off the discussion about an LLM policy.
- Release blocker triage continues. We are now fully caught up and marked a few more issues as blockers – all of them minor.
- We unmarked several issues as blockers and marked #23131 as such instead, because it was the root cause for them all. We then spent a long time deliberating what to do about it and debating approaches. We have not reached a conclusion yet and will be keeping an eye on it.
Sync Pod-Simple-1.48 into blead
From Changelog:
3.48 2026-04-05
- Add sections to manpage URLs Peter Oliver++
- Better fallback when no HTML::Entities Tomasz Konojacki++
- Don't call each on an anonymous hash Niko Tyni++
- Fix encoding of Unicode URL fragments Graham Knop++
Fix documentation: specify when foreach iterates an array directly If the list contains more than a single array, it copies the values, so no confusion happens.
We're avid Perl programmers but we have been really wanting to get into Haskell or Erlang or something similar, though we don't know where to start. Any languages you guys recommend? if so, send some good tutorials [or give us a rundown yourself :>]
We must add that we're looking for pure-ish functional languages. Lisp syntax doesn't really sit right with us either so we don't really wish to use those.
edit [2026-04-05]: clarified why we don't accept lisp languages as suggestions
| submitted by /u/briandfoy [link] [comments] |
Originally published at Perl Weekly 767
Hi there!
It was interesting to see how Rust started to eat Python from the inside out. Several Python modules and stand-alone tools were rewritten in Rust. Now as I can see there started to be a movement in Perl as well to rewrite some parts in Rust (e.g. PDL). Very interesting.
People quite often ask me how could they find a job? Given that I have been self-employed providing training and contract development work, I don't have a lot of experience in job-hunting, but it seems if you can show your programming, creative, and learning capabilities then you will have a better chance landing a new job. So I recommend that people build some interesting open source project or contribute to some projects. I am mentioning this as I just saw an article about Chandra: Cross-Platform Desktop GUIs in Perl by Robert Acock and at the end of the article he mentions that he is looking for new opportunities. IMHO building new stuff and talking (or writing) about it creates a lot more opportunities in getting a job than only complaining about the lack of jobs. Good luck!
If talking about contribution, probably one of the easiest thing to contribute is a test. It is also way more valuable than many people might think. If you'd like to contribute to an open source Perl project: 1. clone its source code; 2. Generate test coverage report; 3. Find the holes in the tests and write a test. Then repeat. If you are unsure how to do this, luckily the "Testing in Perl" course I am teaching these days will help you. Last time we saw how to generate and interpret test coverage reports. Next time we'll also see how to do mutation testing. You can watch the recordings from the previous sessions (free of charge for 10 more days) and you can join our next live session.
Enjoy Passover, Easter and the rest of your week!
--
Your editor: Gabor Szabo.
Articles
PDL in Rust -- A Native Reimplementation of the Perl Data Language
Very interesting that more of Perl gets a Rust reimplementation.
Chandra: Cross-Platform Desktop GUIs in Perl
"Chandra is a Perl module that lets you build cross-platform desktop applications using the web technologies you already know - HTML, CSS, and JavaScript - while keeping your application logic in Perl.". Sounds interesting to see where it goes.
Introducing Object::Proto — A Prototype Object System for Perl
Manage the health of your CLI tools at scale
"Your services have dashboards, tracing, and alerting. Your CLI tools print to STDOUT and exit. When something breaks, debugging starts at the API gateway -- everything upstream is a black box. This makes no sense."
Discussion
Activity streams library/module for Perl
Perl
This week in PSC (219) | 2026-03-30
The Weekly Challenge
The Weekly Challenge by Mohammad Sajid Anwar will help you step out of your comfort-zone. You can even win prize money of $50 by participating in the weekly challenge. We pick one champion at the end of the month from among all of the contributors during the month, thanks to the sponsor Marc Perry.
The Weekly Challenge - 368
Welcome to a new week with a couple of fun tasks "Make it Bigger" and "Big and Little Omega". If you are new to the weekly challenge then why not join us and have fun every week. For more information, please read the FAQ.
RECAP - The Weekly Challenge - 367
Enjoy a quick recap of last week's contributions by Team PWC dealing with the "Max Odd Binary" and "Conflict Events" tasks in Perl and Raku. You will find plenty of solutions to keep you busy.
Max Conflict
Arne demonstrates how Raku can help with the "Max Conflict" problem using Raku's built-in support for intervals and sets. By using Raku's expressive syntax (like ranges and Bags), Raku has created an easy-to-read and mathematically intuitive way to solve problems that involve overlapping data.
Overlapping Oddities
The post contains an excellent technical breakdown of both terms using Perl programming language, along with the associated edge cases, such as how to handle timings that cross over midnight using modular arithmetic to resolve them. Additionally, his benchmark testing for different ways to manipulate binary strings provides measurable, statistical data on the efficiency of these algorithms.
Odd Events
The post explores time-interval logic and complexities of "wrap-around" (i.e., time zone?) events in a mathematically rigorous and fascinating way. Specifically, by introducing the idea of "complementary" events and making use of an ingenious duration-sum inequality, he presents an elegant, universal formula for simplifying conflict detection across the Perl and J programming languages.
The Weekly Challenge 367
The post provides a very pragmatic and functional dual language reference to assist in solving PWC 367. He uses string replication to solve the binary operation task and provides a simple mathematical overlap condition $max(start1, start2) < min(end1, end2)$ for the scheduling task. Therefore his post shows readers how to write readable, fast and efficient code in Perl and Python.
overlapping intervals
The article provides a flexible technical overview of PWC 367: Solutions to the two tasks implemented in both Raku and PL/Perl for PostgreSQL (PostgreSQL's Procedural Language/Perl). He presents Raku as a convenient and efficient way of working with high-level string and array operations to concisely solve the binary and interval task while at the same time illustrating how database-level functions can be written using the same logical patterns as the ones he used for both the Raku and PL/Perl implementations of the two tasks.
Perl Weekly Challenge 367
This is a perspective piece reviewing the problems faced in physics, providing elegant one-liners using Perl and/or robust scripts. In particular, solution to events happening at "Conflicting Events" is unique in that he treats time as a circular interval and takes offsets of + or -1440 minutes so that he can accurately determine whether two events overlap even if one of the events crosses the boundary of midnight by using the degree of circularity. This demonstrates an extensive application of mathematics to derive the proper ways to schedule events over time.
Odd Conflicts
The post demonstrates his focus on maintaining clean code and code maintainability. The use of meaningful variable names and decomposing the time conversion into logical sub-steps allowed him to take a potentially complicated problem of overlapping time zones and present it in a clear and concise manner via a mathematically sound Perl implementation.
Conflicting Every Odd
The post offers a great demonstration of multi-language support for PWC 367, using Raku, Perl, Python, and Elixir to provide solutions. The technical review is particularly good at comparing different programming language techniques and how each can be uniquely applied to provide elegant and precise solutions for string manipulation and scheduling conflicts using Elixir's pattern matching and Raku's dynamic coercion as examples of differences between the languages.
Bits of conflicts
The post features a very well thought out, extensive solution in Perl that uses string multiplication to create the largest odd binary number. He gives us a lin2dec utility that helps validate his solutions. He also provides an excellent explanation of the underlying mathematical principles behind the task he has undertaken.
The Weekly Challenge - 367: Max Odd Binary
The post gives a very complete and well supported solution to the "Max Odd Binary" challenge using Perl, using the principles of defensive programming, through good input validation. The way he technically describes his solution is very helpful since he is able to explain perl specific idioms such as tr///, and also provide good alternatives to count bits when using safe and/or optimised methods so that your code will not fail.
The Weekly Challenge - 367: Conflict Events
The second task solution provides a consistent, well-understood and detailed Perl implementation for identifying conflicting events by accentuating how to process time-based intervals that cross midnight boundaries. The use of a simple mathematical representation (normalisation of times to minutes), and an efficient "shift" loop to address the next day results in an exceptional algorithm that greatly simplifies complex overlapping logic.
The Weekly Challenge #367
The thoughtful way Robbie highlights some of the ambiguity surrounding date-less times in the "Conflict Events" task. By specifically stating what the assumption of "next-day" means for events that occur before a reference time, he offered a reasonable basis upon which to solve for overlaps between intervals of time that is both mathematically accurate and practically viable.
Binary Conflict
The includes solutions in both the Raku and Crystal languages. The way he solves Task#2 is particularly interesting because of how he looks at quantising time; by looking at events as ranges of minutes and splitting events that span midnight into two spans, he has developed a very mathematically sound and clean way to check for non-emptiness of intersecting intervals.
Maximum conflict
Simon Green used the Perl and Python programming language to produce solutions for Task 1 and Task 2. One of Simon's solutions was for the "Conflict Events" problem and it is very good because he used the range objects provided in Python, and generated individual minutes using sets, to determine where there are overlaps by tracking through time; his method provides a good way of handling events that cross over to another day.
Weekly collections
NICEPERL's lists
Great CPAN modules released last week;
MetaCPAN weekly report.
Event reports
Koha Hackfest 2026 in Marseille
Events
Perl Maven online: Testing in Perl - part 3
April 9, 2026
Perl Maven online: Testing in Perl - part 4
April 16, 2026
Perl Toolchain Summit 2026
April 23-26, 2026
The Perl and Raku Conference 2026
June 26-29, 2026, Greenville, SC, USA
You joined the Perl Weekly to get weekly e-mails about the Perl programming language and related topics.
Want to see more? See the archives of all the issues.
Not yet subscribed to the newsletter? Join us free of charge!
(C) Copyright Gabor Szabo
The articles are copyright the respective authors.
| submitted by /u/swe129 [link] [comments] |
Update metadata for new ExtUtils::ParseXS release
Anyone else thinking about this? I need a way to communicate between instances of https://sourceforge.net/projects/cclite2/.
I'm moving away from RabbitMq to MQTT, mainly because Rabbit is hard to build/configure and is overkill for what I need. So I'm just thinking about using just the record format(s)
{ "@context": "https://www.w3.org/ns/activitystreams", "id": "http://example.org/foo", "type": "Note", "name": "My favourite stew recipe", "attributedTo": { "id": "http://joe.website.example/", "type": "Person", "name": "Joe Smith" }, "published": "2014-08-21T12:34:56Z" } This is pretty easy since I'm also using Mojolicious, but I wondered whether anyone else was working in this area?
[link] [comments]

A few days ago, when we announced pperl's native module strategy
on Reddit, someone asked about PDL support.
We replied:
"PDL will be supported." — to which u/fuzzmonkey35
responded: "We will live in glorious times when this happens."
Well. It happened.
We (as in: we and our AIs) reimplemented PDL (the Perl Data Language) from scratch in Rust — not a binding, not an FFI wrapper, but a ground-up reimplementation of the core engine. 15 data types, N-dimensional arrays, broadcasting, operator overloading, reductions, linear algebra, transcendental math — all in pure Rust, integrated as a native module into our pperl next-gen Perl5 platform.
use PDL; my $a = pdl([1, 2, 3]); my $b = pdl([10, 20, 30]); say $a + $b; # [11 22 33] say ($a * $b)->sum; # 140 say sin(pdl([0, 3.14159/2, 3.14159]));
45 tests, all green. Same Perl syntax. No XS. No C. No libpdl.
Why Reimplement PDL?
PDL is one of Perl's crown jewels — a fast, expressive N-dimensional array language that has served the scientific Perl community since 1996. It is also one of the significantly entangled XS modules in the ecosystem: C code, PP code generation, Inline::C, compiler toolchain dependencies, and a build process that assumes intimate knowledge of perl's internals.
Our Perl runtime — pperl (Parallel Perl) — is a from-scratch Perl 5 interpreter written in Rust with a Cranelift JIT and automatic parallelization via Rayon. It does not support XS. That is a deliberate architectural decision: XS ties modules to CPython-style C internals, which makes JIT compilation across module boundaries impossible and thread safety a minefield.
But "no XS" means PDL cannot simply be loaded. It must be reimplemented. And since we already have a Rust runtime, the natural answer is: reimplement PDL's engine in Rust, with zero C dependencies, and expose it through pperl's native module interface — the same mechanism we use for List::Util, Storable, Fcntl, and other core XS modules.
What's Implemented
The Rust PDL crate (rust-pdl) implements the core engine.
The pperl native module (src/native/PDL/) bridges it into
Perl space as a blessed PDL object, with operator
overloading and method dispatch working exactly as you'd expect.
Core Infrastructure
- 15 data types: Byte, SByte, Short, Ushort, Long, ULong, LongLong, ULongLong, Indx, Float, Double, LDouble, CFloat, CDouble, CLDouble
- N-dimensional arrays: arbitrary rank, with dims/strides/nelem metadata
- Automatic type promotion: integer → float → complex, following PDL's complexity ordering
- Broadcasting (formerly "threading"): implicit dimension negotiation for binary operations
- Bad value handling: sentinel values for integers, NaN for floats
Constructors
my $v = pdl([1, 2, 3]); # 1D vector my $m = pdl([[1,2],[3,4]]); # 2D matrix my $z = zeroes(4); # [0 0 0 0] my $o = ones(3); # [1 1 1] my $s = sequence(5); # [0 1 2 3 4] my $x = xvals(3, 3); # x-coordinate grid
Arithmetic & Comparison (Overloaded)
my $a = pdl([1, 2, 3]); my $b = pdl([10, 20, 30]); $a + $b; # [11 22 33] $b - $a; # [9 18 27] $a * $b; # [10 40 90] $b / $a; # [10 10 10] $a == $b; # [0 0 0] (element-wise, returns Byte PDL) sqrt($a); # [1 1.414.. 1.732..]
Reductions
my $x = pdl([1, 2, 3, 4, 5]); $x->sum; # 15 $x->avg; # 3 $x->min; # 1 $x->max; # 5my $m = pdl([[1,2],[3,4]]);
$m->sumover; # [3 7] (reduce along last dimension)
Linear Algebra & Selection
pdl([1,2,3])->inner(pdl([4,5,6])); # 32 (dot product)my $mask = pdl([0, 1, 0, 1, 1]);
$mask->which; # [1 3 4] (indices of true)
Transcendental Math
sin(pdl([0, 3.14159/2])); # [0 1] cos(pdl([0, 3.14159])); # [1 -1] exp(pdl([0, 1])); # [1 2.71828..] log(pdl([1, 2.71828])); # [0 1]
Additional math functions include tan,
asin, acos, atan,
sinh, cosh, tanh,
ceil, floor, rint,
erf, erfc, lgamma.
Architecture: Two Layers
The implementation is split into two clean layers:
1. The rust-pdl crate — a standalone
Rust library (~7,100 lines across 23 modules) that knows nothing about
Perl. It implements the PDL data structure, all operations, type
dispatch, broadcasting, and display. It could be used from any Rust
project.
2. The pperl native module
(src/native/PDL/) — the bridge layer. It converts between
Perl SVs and Rust Pdl structs, registers XS-style
functions, manages operator overloading, and handles the blessed hash
pattern that Perl-side PDL code expects. PDL objects are blessed
hashrefs containing a raw pointer to a heap-allocated
Box<Pdl>.
The module hierarchy mirrors PDL's own:
PDL — top-level boot, exports pdl/zeroes/ones/sum/...
PDL::Core — constructors, accessors, DESTROY
PDL::Ops — arithmetic & comparison operators
PDL::Ufunc — reductions (sum, avg, min, max, sumover, ...)
PDL::Math — transcendental functions
PDL::Primitive — inner, matmult, which, where, clip, append
PDL::Slices — slice("1:3"), slice("0:-1:2"), etc.
PDL::Basic — sequence, xvals, yvals, zvals
Why Not Just Bind to C PDL?
Three reasons:
JIT integration. pperl's Cranelift JIT can compile hot loops operating on PDL data — but only if the data structures are Rust-native. A C library behind FFI is an opaque wall to the JIT. With
rust-pdl, the JIT can in principle inline array operations into compiled machine code.Thread safety. pperl's Rayon-based
auto-parallelization needs data structures that are safe to share
across threads. Rust's ownership model provides this at compile
time. C PDL's reference-counted, globally-mutated state does
not.No toolchain dependency. A pure Rust PDL means
no C compiler, no make, no XS, no Inline::C, no PP code
generation.cargo buildand it's done. The entire PDL
engine compiles as part of the pperl binary.
Performance
Look at the test harness output:
PDL/010-constructor.t ... +5:ok (17/17) (p5: 81ms / +5: 5ms) PDL/020-arithmetic.t ... +5:ok (12/12) (p5: 82ms / +5: 4ms) PDL/030-reductions.t ... +5:ok (10/10) (p5: 81ms / +5: 5ms) PDL/040-math.t ... +5:ok (6/6) (p5: 85ms / +5: 4ms)
These numbers deserve context. The p5 column is
standard perl5 with PDL loaded from CPAN — 81-85ms per test file
because PDL's startup pulls in dozens of modules, compiles PP code,
and initializes the type system. The +5 column is pperl:
4-5ms. That's a 16-20x startup advantage, because the
entire PDL engine is compiled into the pperl binary at build time.
There is no module loading, no PP compilation, no dynamic linking — just
a function pointer table registration.
For large-array compute workloads, the Rust engine's performance matches PDL's C core — unsurprisingly, since tight numeric loops in Rust compile to essentially the same LLVM IR as C. The real win comes when pperl's JIT can fuse Perl-level loops with PDL array operations — something that is structurally impossible with XS-based PDL.
What's Next
The current implementation covers PDL's core operations — enough to run real scientific code. The roadmap:
- Slicing and dataflow: The
slice()infrastructure exists but the full lazy-evaluation dataflow engine (parent↔child transformations) is Phase 3 - PDL::PP equivalent: A Rust macro system for declaring typed operations — the foundation for community-contributed domain modules
- Domain modules: Signal processing, image processing, fitting — following PDL's module ecosystem
- JIT fusion: Cranelift compilation of PDL operations within Perl loops — the ultimate payoff of the pure-Rust architecture
The Bigger Picture
PDL in Rust is a proof point for pperl's native module strategy. The Perl community has long been told that "no XS support" means "no real modules". We disagree. List::Util, Storable, Fcntl, Scalar::Util, Sub::Util, Sys::Hostname, PadWalker — all reimplemented as native Rust modules in pperl, all passing their test suites. PDL is the most ambitious one yet: a full numerical computing engine, not just a utility module.
The pattern is always the same: read the original C/XS implementation, understand the data structures and control flow, and transliterate faithfully into Rust. No shortcuts. No "simplified subset". The Perl interface must behave identically — because existing Perl code must run unchanged.
Pure Perl will never be as fast as XS? We are not sure about that. When the JIT can see through the module boundary and the parallelizer can distribute across cores, "pure Perl + Rust engine" may well surpass "Perl + C library behind an opaque FFI wall".
PDL in Rust is the beginning of that argument.
The rust-pdl crate and its pperl integration are part
of the pperl codebase, maintained by PetaMem s.r.o.
— Richard C. Jelinek, PetaMem s.r.o.
Task 1, Maximum Odd Binary
It's the week of the Artemis 2 mission to the moon, and we have a problem about odd numbers. I'd call that a Space Oddity.
Task Description
You are given a binary string that has at least one ‘1’.
Write a script to rearrange the bits in such a way that the resulting binary number is the maximum odd binary number and return the resulting binary string. The resulting string can have leading zeros.
-
Example 1: Input:
$str = "1011", Output:"1101" -
Example 2: Input:
$str = "100", Output:"001" -
Example 3: Input:
$str = "111000", Output:"110001" -
Example 4: Input:
$str = "0101",Output:"1001" -
Example 5: Input:
$str = "1111", Output:"1111"
Task Cogitation
To be as large as possible, a binary number must have all its 1s in the most-significant bits. To be odd, the least significant bit must be a one. So, what has to happen is that all the 1s shift to the left, one shifts to the right, and all the zeroes form a space between them.
I see three possible solutions:
- Sort: sort the bits so that the 1s are on the left, the 0s on the right, and then rotate a 1 into the right-most position.
- Split: make a pass over the bits, creating a pile of 1s and a pile of 0s. Form the required output string from the piles.
- Count: There's no need to move the bits around. Count how many 1s there are. Conjure a string of 1s, and a string of 0s.
Task Implementation
Solution 1: Sorting
sub mobSort($str)
{
my @bits = sort { $b cmp $a } split(//, $str);
push @bits, shift @bits;
return join("", @bits);
}
Notes:
- Sort in descending order to put 1s on the left
- Rotate by taking the bit on the left and pushing it onto the right.
Solution 2: Splitting
sub mobSplit($str)
{
my @bit = ( "", "" );
$bit[$_] .= $_ for split //, $str;
substr($bit[1], -1, 0, $bit[0]);
return $bit[1];
}
Notes:
- I want a group of 0s and a group of 1s. That's an array of size two, conveniently indexed by
[0]and[1]. - For each 0 or 1, append to its string.
- To form the output, use the four-argument form of
substr, which replaces a substring. In the string of 1s, insert the string of 0s just before the zero-length string in front of the last bit.
Solution 3: Counting
sub mobCount($str)
{
my $n = length($str);
my $zero = ($str =~ tr/0/0/);
return ("1" x ($n-$zero-1)) . ("0" x $zero) . ("1");
}
Notes:
- The output will be the same length as the input.
- The number of zeroes can be counted by the Perl FAQ idiom of using the
tr///operator -- it returns the number of substitutions. - Now we know how any 1s and 0s we need, so replicate them appropriately.
Task Benchmark
It's too trivial a problem to be concerned about performance, but let's benchmark it anyway.
Rate sorting splitting counting
sorting 128205/s -- -28% -95%
splitting 178571/s 39% -- -93%
counting 2500000/s 1850% 1300% --
I'm not surprised that sorting is slowest (it's an O(n_log_n) algorithm). I am a little surprised how much faster counting is.
Task 2, Conflict Events
Task Description
You are given two events' start and end time.
Write a script to find out if there is a conflict between the two events. A conflict happens when two events have some non-empty intersection.
-
Example 1:
- Input:
@event1 = ("10:00", "12:00")@event2 = ("11:00", "13:00") - Output: true
- Both events overlap from "11:00" to "12:00".
- Input:
-
Example 2:
- Input:
@event1 = ("09:00", "10:30")@event2 = ("10:30", "12:00") - Output: false
- Event1 ends exactly at 10:30 when Event2 starts. Since the problem defines intersection as non-empty, exact boundaries touching is not a conflict.
- Input:
-
Example 3:
- Input:
@event1 = ("14:00", "15:30")@event2 = ("14:30", "16:00") - Output: true
- Both events overlap from 14:30 to 15:30.
- Input:
-
Example 4:
- Input:
@event1 = ("08:00", "09:00")@event2 = ("09:01", "10:00") - Output: false
- There is a 1-minute gap from "09:00" to "09:01".
- Input:
-
Example 5:
- Input:
@event1 = ("23:30", "00:30")@event2 = ("00:00", "01:00") - Output: true
- They overlap from "00:00" to "00:30".
- Input:
Task Cogitation
The string form of time is annoying to work with. The first thing to do is to convert the time to a minute of the day. As we learned last week, there are 1440 minutes in a day. And as we learned long ago, there are 525,600 minutes in a year, but that's not relevant. Stay focused.
Once we have the times converted, then we have a problem in detecting overlapping ranges. I know how to do that. The examples highlight the edge cases.
Example 5 throws a curve: the ranges can cross midnight. There will be math involving modulo operators.
I'm going to break this down into small pieces.
Task Solution
What I'm going for is: does the range of event 1 overlap with the range of event 2? That means I need a function to see if ranges overlap, which means I need a function to convert times to ranges, which means I need functions to convert time strings to numbers. Let's pop that stack.
Subtask: Time conversion
Very straightforward. I'm assuming times have been validated and are in the specified format.
sub toMinute($hhmm)
{
my ($h, $m) = split(":", $hhmm);
my $minute = $m + 60 * $h;
return $minute;
}
Subtask: Event range
Here's where we isolate the problem of spanning midnight. We figure out how long the event is, using a 24-hour clock modulo operation. Then, replace the beginning of the range by subtracting the duration from the end. That means we might get a negative number for the beginning, but that's okay -- it represents a time before midnight.
sub toRange($minBeg, $minEnd)
{
my $duration = ($minEnd - $minBeg) % 1440;
return [ $minEnd - $duration, $minEnd ];
}
Subtask: Range overlap
Given two ranges, whether they overlap depends on where their edges line up.
I like use enum as a way to introduce a little readability. It's a very efficent module, and quite flexible.
sub isOverlap($range1, $range2)
{
use enum qw( BEG=0 END=1 );
return $range1->[END] > $range2->[BEG]
&& $range1->[BEG] < $range2->[END];
}
Composition of the solution
sub isConflict($event1, $event2)
{
return isOverlap(toRange( map { toMinute($_) } $event1->@* ),
toRange( map { toMinute($_) } $event2->@* ) );
}
Given all the pieces, the solution falls into place.
-
map { toMinute() "-- convert the time strings into pairs of numbers that we can do arithmetic with. -
toRange( map {} )- convert pairs of points in time into ranges that might span midnight. -
isOverlap(...)- the answer is the result of checking for range overlap.
-
App::Staticperl - perl, libc, 100 modules, all in one standalone 500kb file
- Version: 1.5 on 2026-04-04, with 21 votes
- Previous CPAN version: 1.46 was released 4 years, 1 month, 16 days before
- Author: MLEHMANN
-
Catalyst::Action::REST - Automated REST Method Dispatching
- Version: 1.22 on 2026-03-30, with 13 votes
- Previous CPAN version: 1.21 was released 8 years, 3 months, 25 days before
- Author: ETHER
-
CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
- Version: 20260329.001 on 2026-03-29, with 25 votes
- Previous CPAN version: 20260327.002 was released 1 day before
- Author: BRIANDFOY
-
Devel::NYTProf - Powerful fast feature-rich Perl source code profiler
- Version: 6.15 on 2026-03-31, with 199 votes
- Previous CPAN version: 6.14 was released 2 years, 5 months, 12 days before
- Author: JKEENAN
-
Devel::Size - Perl extension for finding the memory usage of Perl variables
- Version: 0.87 on 2026-03-31, with 22 votes
- Previous CPAN version: 0.86_50 was released 1 month, 19 days before
- Author: NWCLARK
-
Dios - Declarative Inside-Out Syntax
- Version: 0.002014 on 2026-04-01, with 24 votes
- Previous CPAN version: 0.002013 was released 1 year, 7 months, 14 days before
- Author: DCONWAY
-
Inline::Module - Support for Inline-based CPAN Extension Modules
- Version: 0.35 on 2026-03-30, with 14 votes
- Previous CPAN version: 0.34 was released 11 years, 1 month, 12 days before
- Author: INGY
-
IPC::Run - system() and background procs w/ piping, redirs, ptys (Unix, Win32)
- Version: 20260402.0 on 2026-04-02, with 39 votes
- Previous CPAN version: 20260401.0 was released the same day
- Author: TODDR
-
LWP - The World-Wide Web library for Perl
- Version: 6.82 on 2026-03-29, with 212 votes
- Previous CPAN version: 6.81 was released 5 months, 6 days before
- Author: OALDERS
-
Module::CoreList - what modules shipped with versions of perl
- Version: 5.20260330 on 2026-03-29, with 45 votes
- Previous CPAN version: 5.20260320 was released 8 days before
- Author: BINGOS
-
Module::Metadata - Gather package and POD information from perl module files
- Version: 1.000039 on 2026-04-03, with 14 votes
- Previous CPAN version: 1.000038 was released 2 years, 11 months, 5 days before
- Author: ETHER
-
Mouse - Moose minus the antlers
- Version: v2.6.2 on 2026-04-04, with 63 votes
- Previous CPAN version: v2.6.1 was released 3 months, 14 days before
- Author: SYOHEX
- perl - The Perl 5 language interpreter
- Version: 5.042002 on 2026-03-29, with 2251 votes
- Previous CPAN version: 5.042001 was released 21 days before
- Author: SHAY
-
Pod::Simple - framework for parsing Pod
- Version: 3.48 on 2026-04-04, with 20 votes
- Previous CPAN version: 3.47 was released 10 months, 19 days before
- Author: KHW
-
Sidef - The Sidef Programming Language - A modern, high-level programming language
- Version: 26.04 on 2026-04-01, with 122 votes
- Previous CPAN version: 26.01 was released 2 months, 18 days before
- Author: TRIZEN
-
SPVM - The SPVM Language
- Version: 0.990153 on 2026-03-28, with 36 votes
- Previous CPAN version: 0.990152 was released 2 days before
- Author: KIMOTO
-
Sys::Virt - libvirt Perl API
- Version: v12.2.0 on 2026-04-01, with 17 votes
- Previous CPAN version: v12.1.0 was released 29 days before
- Author: DANBERR
-
Test2::Harness - A new and improved test harness with better Test2 integration.
- Version: 1.000164 on 2026-04-01, with 28 votes
- Previous CPAN version: 1.000164 was released the same day
- Author: EXODIST
-
WebService::Fastly - an interface to most facets of the [Fastly API](https://www.fastly.com/documentation/reference/api/).
- Version: 14.01 on 2026-03-31, with 18 votes
- Previous CPAN version: 14.00 was released 1 month, 14 days before
- Author: FASTLY
-
YAML::Syck - Fast, lightweight YAML loader and dumper
- Version: 1.44 on 2026-04-02, with 18 votes
- Previous CPAN version: 1.43 was released 1 day before
- Author: TODDR
This is the weekly favourites list of CPAN distributions. Votes count: 65
Week's winner: App::FatPacker (+2)
Build date: 2026/04/04 19:54:08 GMT
Clicked for first time:
- App::Chit - chat with AI from the command line
- App::prepare4release - prepare a Perl distribution for release (skeleton)
- Context::Preserve - Run code after a subroutine call, preserving the context the subroutine would have seen if it were the last statement in the caller
- Data::HashMap::Shared - Type-specialized shared-memory hash maps for multiprocess access
- DB::Handy - Pure-Perl flat-file relational database with DBI-like interface
- EV::Websockets - WebSocket client/server using libwebsockets and EV
- Rex::LibSSH - Rex connection backend using Net::LibSSH (no SFTP required)
- Task::Kensho::All - Install all of Task::Kensho
- WWW::Tracking - universal website visitors tracking
Increasing its reputation:
- App::cpanminus (+1=287)
- App::CPANTS::Lint (+1=2)
- App::FatPacker (+2=47)
- Capture::Tiny (+1=106)
- Carp (+1=78)
- Complete::Bash (+1=6)
- Complete::Getopt::Long (+1=2)
- Cpanel::JSON::XS (+1=47)
- Data::Password::zxcvbn (+1=9)
- Devel::Cover (+1=105)
- Devel::NYTProf (+1=199)
- ExtUtils::MakeMaker (+1=65)
- Feersum (+1=15)
- File::Map (+1=25)
- File::Temp (+1=72)
- Getopt::Long (+1=129)
- Getopt::Long::Complete (+1=16)
- Getopt::Long::More (+1=3)
- HTTP::Tiny (+1=116)
- HTTP::Tiny::Mech (+1=4)
- Inline::Module (+1=14)
- JSON::MaybeXS (+1=48)
- JSON::Schema::Modern (+1=10)
- Keyword::Simple (+1=18)
- MCE (+1=112)
- MetaCPAN::Client (+1=28)
- Module::CoreList (+1=45)
- Mojolicious (+1=511)
- Multi::Dispatch (+1=4)
- OpenAPI::Modern (+1=5)
- Path::Tiny (+1=194)
- PathTools (+1=85)
- PDF::API2 (+1=33)
- PDF::Builder (+1=7)
- perl (+1=443)
- Perl::Critic (+1=136)
- Perl::Tidy (+1=148)
- Pod::Usage (+1=47)
- POE (+1=55)
- POE::Component::Curl::Multi (+1=3)
- POE::Component::IRC (+1=14)
- Reply (+1=64)
- Scalar::List::Utils (+1=185)
- Software::License (+1=18)
- Task::Kensho (+1=122)
- Term::ANSIColor (+1=69)
- Term::QRCode (+1=4)
- Term::ReadLine::Gnu (+1=25)
- Test::Harness (+1=66)
- Test::Simple (+1=200)
- Time::Piece (+1=68)
- TOML::Tiny (+1=11)
- Type::Tiny (+1=149)
- WWW::Mechanize (+1=104)
- WWW::Mechanize::Cached (+1=6)
Building desktop applications has traditionally been a challenge in the Perl ecosystem. While we have excellent tools for web development, backend services, and system administration, creating native feeling desktop apps often meant learning and wrestling with complex GUI toolkits or abandoning Perl altogether.
Chandra attempts to change that equation.
What is Chandra?
Chandra is a Perl module that lets you build cross-platform desktop applications using the web technologies you already know — HTML, CSS, and JavaScript — while keeping your application logic in Perl. Under the hood, it leverages native webview components (WebKit on macOS/Linux, Edge on Windows) to render your UI, giving you the best of both worlds: native performance with web flexibility.
use Chandra::App;
my $app = Chandra::App->new(
title => 'My First App',
width => 800,
height => 600,
);
$app->set_content('<h1>Hello from Perl!</h1>');
$app->run;
That's it. Five lines to create a windowed application.
The Bridge Between Worlds
The real power of Chandra lies in its seamless bidirectional communication between Perl and JavaScript. You can expose Perl functions to your frontend with a simple bind call:
my $app = Chandra::App->new(title => 'Counter');
my $count = 0;
$app->bind('increment', sub {
$count++;
return $count;
});
$app->bind('decrement', sub {
$count--;
return $count;
});
$app->set_content(<<'HTML');
<!DOCTYPE html>
<html>
<body>
<h1 id="counter">0</h1>
<button onclick="inc()">+</button>
<button onclick="dec()">-</button>
<script>
async function inc() {
let val = await window.chandra.invoke('increment');
document.getElementById('counter').textContent = val;
}
// or
function dec() {
window.chandra.invoke('decrement').then(function(val) {
document.getElementById('counter').textContent = val;
});
}
</script>
</body>
</html>
HTML
$app->run;
Your JavaScript calls window.chandra.invoke('increment'), Perl increments the counter, and the result flows back as a Promise. JSON serialization happens automatically.
Building UIs the Perl Way
If you prefer constructing your UI in Perl rather than writing raw HTML strings, Chandra::Element provides a DOM-like interface:
use Chandra::Element;
my $ui = Chandra::Element->new({
tag => 'div',
class => 'container',
style => { padding => '20px', background => '#f5f5f5' },
children => [
{ tag => 'h1', data => 'Welcome' },
{
tag => 'button',
data => 'Click Me',
onclick => sub {
my ($event, $app) = @_;
print "Button clicked!\n";
},
},
],
});
$app->set_content($ui);
Event handlers are automatically wired up — when the button is clicked, your Perl callback fires.
Single-Page Apps with Built-in Routing
Modern desktop apps often benefit from SPA-style navigation. Chandra has you covered:
my $app = Chandra::App->new(title => 'My SPA');
$app->layout(sub {
my ($body) = @_;
return qq{
<nav>
<a href="/">Home</a>
<a href="/settings">Settings</a>
</nav>
<div id="chandra-content">$body</div>
};
});
$app->route('/' => sub {
return '<h1>Home</h1><p>Welcome!</p>';
});
$app->route('/settings' => sub {
return '<h1>Settings</h1><p>Configure your app...</p>';
});
$app->route('/user/:id' => sub {
my (%params) = @_;
return "<h1>User $params{id}</h1>";
});
$app->run;
Navigation happens client-side with no page reloads — just like a web SPA, but in a native window.
Native Integration Done Right
Chandra doesn't just wrap a browser. It provides genuine desktop integration:
Native Dialogs
# File picker
my $path = $app->dialog->open_file(title => 'Select a file');
# Directory picker
my $dir = $app->dialog->open_directory;
# Save dialog
my $save_path = $app->dialog->save_file(
title => 'Save As',
default => 'document.txt',
);
# Alert dialogs
$app->dialog->info(message => 'Operation complete!');
$app->dialog->error(message => 'Something went wrong');
System Tray
use Chandra::Tray;
my $tray = Chandra::Tray->new(
app => $app,
icon => 'icon.png',
tooltip => 'My App',
);
$tray->add_item('Show Window' => sub { $app->show });
$tray->add_separator;
$tray->add_item('Quit' => sub { $app->terminate });
$tray->show;
Persistent Storage
my $store = $app->store; # ~/.chandra/<app-name>/store.json
$store->set('theme', 'dark');
$store->set('window.width', 1200);
$store->set('recent_files', ['/path/a', '/path/b']);
my $theme = $store->get('theme'); # 'dark'
Dot notation for nested keys, automatic JSON serialization, atomic writes with file locking — storage that just works.
Hot Reload
Wtch your source files and refresh automatically:
$app->watch('lib/', sub {
my ($changed) = @_;
print "Files changed: @$changed\n";
$app->set_content(build_ui());
$app->refresh;
});
$app->run;
DevTools
Need to inspect elements or debug JavaScript? Enable developer tools:
my $app = Chandra::App->new(
title => 'Debug App',
debug => 1, # Press F12 to open DevTools
);
Platform Support
Chandra works across the major desktop platforms:
- macOS — Uses WebKit via WKWebView
- Linux — Uses WebKitGTK
- Windows — Uses MSHTML/Edge
Getting Started
Install from CPAN:
cpanm Chandra
Or from source:
perl Makefile.PL
make
make install
Then dive into the examples directory for working demonstrations of all major features or I also have several modules already available for you to test:
A Side note: I'm currently on look out for a new contract or permanent opportunity. If you know of any relevant openings, I'd appreciate hearing from you - email@lnation.org
I'm using the AD DS28EC20 1-wire EEPROM (originally from Maxim I believe), and I'm trying to use the 32 byte Extended Read with CRC function (command 0xA5). I'm hoping to get some help from some of the CRC experts on this board. My interest would be an implementation in Python, Perl, or C.
https://www.analog.com/en/products/ds28ec20.html
![EXTENDED READ MEMORY[A5h] command description](https://i.sstatic.net/ksPGhUb8.png)

I have written some data to the first 32 bytes of memory. The following is the 32 bytes of data read plus the 0xA5 command and the two 0x00 address bytes - and the calculated CRC. So I believe that 35 bytes altogether, including the command byte 0xA5 and the two 0x00 address bytes are needed to reproduce the CRC.
0xA5
0x00
0x00
0x03
0x04
0x05
0x06
0x07
0x08
0x09
0x10
0x11
0x12
0x13
0x14
0x15
0x16
0x17
0x18
0x19
0x20
0x21
0x22
0x23
0x24
0x25
0x26
0x27
0x28
0x29
0x30
0x31
0x32
0x33
0x34
CRC: 0x1181 4481 0001000110000001
I created a down-and-dirty Perl script to calculate the data, which reads a file with the 35 bytes of data.
The data to read from the file, and the script:
0xA5
0x00
0x00
0x03
0x04
0x05
0x06
0x07
0x08
0x09
0x10
0x11
0x12
0x13
0x14
0x15
0x16
0x17
0x18
0x19
0x20
0x21
0x22
0x23
0x24
0x25
0x26
0x27
0x28
0x29
0x30
0x31
0x32
0x33
0x34
#!/usr/bin/env perl
use strict;
use warnings;
my($file_old, $file_new) = @ARGV;
my $index = 0;
my $num;
my $num2;
my @number_set_1;
my $crc;
my $crc_init = 0;
#my $crc_init = 0xFFFF;
my $crc_n;
my $poly = hex("0x8005");
my $poly_reflected = hex("0xA001");
die "$file_old name must have at least three chars, $!" if length($file_old) < 3;
die "$file_new name must have at least three chars, $!" if length($file_new) < 3;
open (IN, "<$file_old") or die "Can't open $file_old, $!";
open (OUT, ">$file_new") or die "Can't open $file_new, $!";
print "\nPOLY: $poly \n\n";
while(my $line = <IN>) {
if(!($line !~ /\S/)) {
chomp($line);
#print "At Line Number $.: $line";
#print OUT "$line";
$number_set_1[$index] = hex($line);
$num = pack("C", $number_set_1[$index]); # "C" = unsigned char (byte)
$num2 = unpack("C",$num);
#print "$index $number_set_1[$index] \n";
print "$index $number_set_1[$index] $num2 \n";
$index ++;
}
}
print"\n\n";
printf("POLY = 0X%04X:\n\n", $poly);
$crc = $crc_init;
for(my $i = 0; $i < 34; $i++) {
$crc ^= $number_set_1[$i];
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly : $crc >> 1;
print "$i $number_set_1[$i] $crc\n";
}
$crc_n = ~$crc & 0xFFFF;
print"\nCRC: $crc\nCRC_N: $crc_n\n";
printf("\nCRC: 0X%04X\nCRC_N: 0X%04X\n", $crc, $crc_n);
print"\n\n";
printf("POLY = 0X%04X:\n\n", $poly_reflected);
$crc = $crc_init;
for(my $i = 0; $i < 34; $i++) {
$crc ^= $number_set_1[$i];
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
$crc = $crc & 1 ? ($crc >> 1) ^ $poly_reflected : $crc >> 1;
print "$i $number_set_1[$i] $crc\n";
}
$crc_n = ~$crc & 0xFFFF;
print"\nCRC: $crc\nCRC_N: $crc_n\n";
printf("\nCRC: 0X%04X\nCRC_N: 0X%04X\n\n", $crc, $crc_n);
close IN;
close OUT;
Here is the output from the script:
POLY: 32773
0 165 165
1 0 0
2 0 0
3 3 3
4 4 4
5 5 5
6 6 6
7 7 7
8 8 8
9 9 9
10 16 16
11 17 17
12 18 18
13 19 19
14 20 20
15 21 21
16 22 22
17 23 23
18 24 24
19 25 25
20 32 32
21 33 33
22 34 34
23 35 35
24 36 36
25 37 37
26 38 38
27 39 39
28 40 40
29 41 41
30 48 48
31 49 49
32 50 50
33 51 51
34 52 52
POLY = 0X8005:
0 165 43780
1 0 23721
2 0 20318
3 3 49992
4 4 9410
5 5 42272
6 6 37536
7 7 34199
8 8 44417
9 9 14508
10 16 62526
11 17 48624
12 18 36536
13 19 24973
14 20 57190
15 21 59865
16 22 7657
17 23 6685
18 24 51997
19 25 23753
20 32 36697
21 33 59529
22 34 25067
23 35 63591
24 36 31227
25 37 64127
26 38 40959
27 39 34970
28 40 16009
29 41 24637
30 48 29539
31 49 40566
32 50 40091
33 51 55451
CRC: 55451
CRC_N: 10084
CRC: 0XD89B
CRC_N: 0X2764
POLY = 0XA001:
0 165 31680
1 0 20603
2 0 8976
3 3 52578
4 4 10829
5 5 13866
6 6 56631
7 7 5341
8 8 40917
9 9 39326
10 16 25625
11 17 50789
12 18 9862
13 19 28646
14 20 34286
15 21 33732
16 22 23811
17 23 3933
18 24 62414
19 25 24243
20 32 27934
21 33 4141
22 34 1104
23 35 58693
24 36 59428
25 37 49193
26 38 1152
27 39 47685
28 40 60795
29 41 64876
30 48 14845
31 49 21817
32 50 50964
33 51 6791
CRC: 6791
CRC_N: 58744
CRC: 0X1A87
CRC_N: 0XE578
There are 2 passes made, one with the non-reflected poly, and one with the reflected poly. I believe the result that should match the device calculation should be the CRC_N value. As you can see, none of the results match the device output CRC of 0x1181.
Expected to happen: Calculate a CRC of 0x1181 which the device calculated.
What actually happened: CRC calculation of 0x2764 with non-inverted poly 0x8001, and 0xE578 with inverted poly 0xA001.
Any help or thoughts would be welcome.
In Perl regular expressions, (?(DEFINE)(?<NAME>...)) creates a referencable pattern which can be executed by the "recursion" mechanism e.g. (?&NAME). This is useful if the "..." is a parameter with unknown internal capture groups. As perlre alludes to, the (DEFINE) has to be placed at the end of the regex if evaluated in List context (because extra 'undef' values corresponding to inner groups in the "..." will be returned).
I'm wondering why this construct exists in Perl because AFAICT ordinary capture groups can be used by just inserting an (*ACCEPT) to prevent directly executing them.
For example, given $_ = "beforeMIDDLEafter" the following seem to do exactly the same:
/ ^ (\w*?) (?&SUB) (\w*) (*ACCEPT) (?<SUB>MI(D)(?1)LE) /x;
/ ^ (\w*?) (?&SUB) (\w*) (?(DEFINE)(?<SUB>MI(D)(?1)LE)) /x;
Both set @{^CAPTURE)=("before", "after") and both return ("before","after",undef,undef) in list context.
My question is: Are these exactly equivalent? Does the (DEFINE) construct provide any additional or different capability?
Using the T command in the Perl debugger produces a stack backtrace similar to
$ = main::infested called from file 'Ambulation.pm' line 10
@ = Ambulation::legs(1, 2, 3, 4) called from file 'camel_flea' line 7
$ = main::pests('bactrian', 4) called from file 'camel_flea' line 4
(example copied from https://perldoc.perl.org/perldebug). When running the Perl debugger from Emacs (via M-x perldb), such a stack backtrace shows up as regular text in the debugger buffer, without any magic to quickly navigate to one of the mentioned lines in the mentioned files to see its context. I have to open the relevant file by hand and then navigate to the mentioned line. Can this be made more convenient?
Perl's object system is famously flexible. bless a reference, add some accessors, and you're in business. Over the years, modules like Moose, Moo, and Mouse have built increasingly sophisticated layers on top of that foundation — giving us type constraints, roles, lazy attributes, and declarative syntax.
But they all share the same bedrock: a blessed hash reference.
Object::Proto takes a less used approach. It stores objects as blessed arrays, maps property names to slot indices at compile time, and compiles accessors down to custom ops — the same mechanism Perl uses internally for built-in operations. The result is an object system that is type-safe, feature-rich, and significantly faster than the previously mentioned alternatives.
The Basics
use Object::Proto;
object 'Animal',
'name:Str:required',
'sound:Str:default(silence)',
'age:Int';
my $cat = new Animal name => 'Whiskers', age => 3;
print $cat->name; # Whiskers
print $cat->sound; # silence
$cat->age(4); # setter
The object keyword defines a class at compile time. Property specifications use a colon-separated format: name:Type:modifier. No hash lookups at runtime — $cat->name compiles to a direct array slot access.
Both positional and named constructors are supported:
my $cat = new Animal 'Whiskers', 'meow', 3; # positional (fastest)
my $cat = new Animal name => 'Whiskers', age => 3; # named pairs
What You Get
Built-in Types (Zero Overhead)
Any Defined Str Int Num Bool ArrayRef HashRef CodeRef Object
These are checked inline at the C level — no function call, no callback. You can also register custom types in Perl or in XS.
Slot Modifiers
object 'Person',
'name:Str:required',
'email:Str:required:readonly',
'age:Int:default(0)',
'nickname:Str:clearer:predicate',
'settings:HashRef:lazy:builder(_build_settings)',
'parent:Object:weak',
'internal_id:Int:arg(_id)', # constructor uses _id, accessor uses internal_id
'score:Num:trigger(on_score_change)',
'rank:Str:reader(get_rank):writer(set_rank)';
Everything you'd expect from a modern object system: required, readonly, default, lazy, builder, clearer, predicate, reader, writer, trigger, weak references, and arg (init_arg) — all compiled to C/XS.
Inheritance
object 'Animal', 'name:Str:required', 'sound:Str';
object 'Dog',
extends => 'Animal',
'breed:Str';
my $dog = new Dog name => 'Rex', sound => 'Woof', breed => 'Lab';
print $dog->isa('Animal'); # 1
Multiple inheritance and multi-level chains work as youl would expect:
object 'Triathlete',
extends => ['Swimmer', 'Runner'],
'event:Str';
Important note: because Object::Proto objects are arrays and Moo/Moose/Mouse objects are hashes, you cannot inherit from Moo, Moose, or Mouse classes (or vice versa). The internal representations are fundamentally incompatible. If you need to interoperate, use composition (roles or delegation via slots) rather than inheritance.
Roles
Roles work the way you'd expect — define a bundle of slots and method requirements, then compose them into any class. A role can carry its own attributes and demand that consuming classes implement specific methods:
use Object::Proto;
role('Printable', 'format:Str:default(text)');
requires('Printable', 'to_string');
object 'Document',
'title:Str:required',
'body:Str';
with('Document', 'Printable');
package Document;
sub to_string { $_[0]->title . ': ' . $_[0]->body }
The format slot from Printable is merged into Document at define time. If Document didn't provide a to_string method, the with call would croak. You can check role consumption at runtime with Object::Proto::does($obj, 'Printable').
Method Modifiers
You can wrap existing methods without subclassing. before runs code ahead of the original, after runs code after it, and around gives you full control — you receive the original method as $orig and decide whether (and how) to call it:
before 'bark' => sub {
my ($self) = @_;
warn "About to bark...";
};
after 'bark' => sub {
my ($self) = @_;
warn "Done barking.";
};
around 'bark' => sub {
my ($orig, $self, @args) = @_;
return uc $self->$orig(@args);
};
Multiple modifiers can be stacked on the same method. They're stored as linked lists in C, so the dispatch overhead is minimal.
Prototype Chains
This is where Object::Proto gets its name. Like JavaScript's prototype model, any object can delegate to another object. When you access a property that is undef in the current object, the lookup walks up the prototype chain until it finds a defined value:
my $defaults = new Animal name => 'Default', sound => 'silence';
my $cat = new Animal name => 'Whiskers';
$cat->set_prototype($defaults);
print $cat->sound; # 'silence' — resolved from prototype
$cat has no sound of its own, so the accessor walks up to $defaults and returns 'silence'. Setting $cat->sound('meow') writes to $cat directly — prototypes are never mutated by child writes. You can inspect the full chain with Object::Proto::prototype_chain($obj) and measure its depth with Object::Proto::prototype_depth($obj).
Mutability Controls
Objects can be locked (preventing structural changes) or frozen (permanent deep immutability). A frozen object's setters will croak on any attempt to modify — useful for configuration objects, default prototypes, or any value you want to guarantee stays constant:
$obj->lock; # prevent structural changes
$obj->unlock;
$obj->freeze; # permanent immutability — setters will croak
$obj->is_frozen; # true
Locking is reversible; freezing is not. The frozen check is implemented as a single bitflag test in C — it adds no measurable overhead to the normal (unfrozen) code path.
Cloning & Introspection
Object::Proto provides a full introspection API. You can clone objects (the clone is always fresh, unfrozen and unlocked, even if the original wasn't), list properties, inspect slot metadata, and walk the inheritance tree:
my $clone = Object::Proto::clone($obj); # shallow copy, unfrozen
my @props = Object::Proto::properties('Dog');
my $info = Object::Proto::slot_info('Dog', 'name');
my @chain = Object::Proto::ancestors('Dog');
slot_info returns a hashref with everything about a slot: its type, whether it's required, readonly, lazy, has a default, trigger, builder, and so on. Useful for building serializers, form generators, or debugging tools.
Singletons
Need a class that only ever has one instance? Mark it as a singleton and use ->instance() instead of new. The first call constructs the object; every subsequent call returns the same one:
object 'Config', 'db_host:Str', 'db_port:Int';
Object::Proto::singleton('Config');
my $c = Config->instance(db_host => 'localhost', db_port => 5432);
# Config->instance returns the same object every time after first call
This is handled at the C level — no Perl-side state variable or CHECK block trickery.
Function-Style Accessors
For the absolute fastest access, Object::Proto offers function-style accessors that bypass method dispatch entirely. These are compiled to custom ops at compile time — a direct array slot read/write with no entersub, no method resolution, no hash lookup:
use Object::Proto;
object 'Point', 'x:Num', 'y:Num';
Object::Proto::import_accessors('Point');
my $p = new Point 1.0, 2.0;
print x($p); # 1.0 — custom op, not a method call
y($p, 3.0); # set y to 3.0
Performance
Here are benchmarks from a mixed new + set + get workload:
Rate Pure Hash Pure Array XS OO Typed Raw Hash Ref XS func Raw Hash
Pure Hash 1824921/s -- -14% -48% -64% -66% -66% -70%
Pure Array 2124357/s 16% -- -40% -58% -60% -61% -65%
Object::Proto OO 3541833/s 94% 67% -- -31% -34% -35% -41%
Object::Proto T 5114883/s 180% 141% 44% -- -4% -6% -15%
Raw Hash Ref 5331773/s 192% 151% 51% 4% -- -2% -12%
Object::Proto fn 5427162/s 197% 156% 53% 6% 2% -- -10%
Raw Hash 6024884/s 230% 184% 70% 18% 13% 11% --
The function-style path (XS func) runs slightly faster than raw hash reference access — while giving you a real object with type constraints, constructors, and a class hierarchy. The method-style path (XS OO) is nearly 2x faster than a pure-Perl hash-based object.
For hot set + get loops on pre-constructed objects, the XS function path has been measured at over 32 million operations per second — faster than a hash $hash{key} access.
Extending the Type System from XS
If you write XS modules, you can register types with C-level check functions that run at near-zero cost (~5 CPU cycles vs ~100 for Perl callbacks):
/* In your BOOT section */
#include "object_types.h"
static bool check_positive_int(pTHX_ SV *val) {
return SvIOK(val) && SvIV(val) > 0;
}
BOOT:
object_register_type_xs(aTHX_ "PositiveInt", check_positive_int, NULL);
use MyTypes; # registers in BOOT
use Object::Proto;
object 'Counter', 'value:PositiveInt:required';
For Moo/Moose Users: Object::Proto::Sugar
If you prefer declarative Moo-style syntax, Object::Proto::Sugar wraps the XS layer with familiar keywords. Everything compiles down to the same custom ops underneath:
package Animal;
use Object::Proto::Sugar;
has name => ( is => 'rw', isa => Str, required => 1 );
has sound => ( is => 'rw', isa => Str, default => 'silence' );
has age => ( is => 'rw', isa => Int );
sub speak { $_[0]->sound }
package Dog;
use Object::Proto::Sugar;
extends 'Animal';
has breed => ( is => 'rw', isa => Str );
package main;
my $dog = new Dog name => 'Rex', sound => 'Woof', breed => 'Lab';
print $dog->speak; # Woof
print $dog->isa('Animal'); # 1
Every feature from the core module is available through the Sugar interface: lazy, builder, coerce, trigger, predicate, clearer, reader, writer, weak_ref, init_arg, roles (use Object::Proto::Sugar -role / with / requires), method modifiers (before, after, around), and function-style accessors via accessor => 1. The Sugar layer runs at compile time via BEGIN::Lift and then Devel::Hook via BIGIN and UNITCHECK hooks — by the time your first line of runtime code executes, everything has been compiled down to the same custom ops as the raw object keyword.
Sugar with Function-Style Accessors
Add accessor => 1 to a has declaration to get a function style accessor alongside the method style one. Then call import_accessors in your package so the functions are visible when your calling code compiles.
package Point;
use Object::Proto::Sugar;
has x => ( is => 'rw', isa => Num, accessor => 1 );
has y => ( is => 'rw', isa => Num, accessor => 1 );
package main;
Point->import_accessors; # install before compilation
my $p = new Point x => 1.0, y => 2.0;
print x($p); # 1.0 — compiled to custom op
y($p, 3.0); # direct array write
Sugar Benchmarks vs Moo and Mouse
Rate Moo Mouse Sugar Sugar (fn)
Moo 1264320/s -- -2% -55% -60%
Mouse 1290237/s 2% -- -54% -59%
Sugar (method) 2784378/s 120% 116% -- -12%
Sugar (func) 3174093/s 151% 146% 14% --
Sugar method-style is 2.2x Moo. Sugar function-style is 2.5x Moo.
A Note on Compatibility
Once again a reminder: because Object::Proto objects are arrays and Moo/Moose/Mouse objects are hashes, you cannot inherit across the boundary. extends 'My::Moo::Class' will not work. Use role or delegation to bridge the two worlds if needed. Within its own ecosystem — Object::Proto classes inheriting from other Object::Proto classes — everything works seamlessly, including multiple inheritance.
Object::Proto is available on CPAN and licensed under the Artistic License 2.0.
cpanm Object::Proto
cpanm Object::Proto::Sugar # optional, for Moo-like syntax
Mark reginfo_buf as uninitialized to avoid performance regression This is similar to previous commit "Mark svpv_buf as uninitialized to avoid performance regression". With this commit, even when compiled with `-ftrivial-auto-var-init=zero`, Perl still exhibits good performance during regex matching operations.
RMG improvements arising from 5.43.9 release notes * Make bold advice to ignore pedantic POD check * Rework the "Update perldelta" section * Change perlhist.pod commit message to include version number Co-authored-by: Philippe Bruhat (BooK) <book@cpan.org> Co-authored-by: Thibault Duponchelle <thibault.duponchelle@gmail.com>
Finally - GTC 2.0, an all in one color library, is released ! This post will not be rehash (of) the (very) fine manual, but give you a sense what you can achieve with this software and why it is better than any other lib of that sort on CPAN. If you like to look under the hood of GTC, please read my last post.
When I released GTC 1.0 in 2022, it had 4 major features:
1. computing color gradients, between 2 colors in RGB
2. computing complementary colors in HSL
3. translating color names from internal constant set into RGB values
4. converting RGB to HSL and back
The HSL support allowed to add and subtract lightness and saturation (make colors darker, or lighter make them more pale or colorful). And by mentioning a very rudimentary distance computation and color blending we reached the bottom of the barrel.
GTC 2.0 expanded in all areas by a manyfold. Going from 2 (RGB and HSL) to now 17 color spaces (soon ~25) has a large effect. Not only being able to read and write color values from 17 spaces makes GTC much more useful, but also computing a gradient and measuring the distance in different spaces gives you options. Some spaces are optimized for human perception (OKLAB or CIELUV) other you would choose for technical necessity. Especially OKLAB and OKHCL are the hype (for a while) and GTC is the only module in CPAN supporting it. Almost all methods (beside ''name'' and ''complement'') let you choose the color space, the method will be computed in. And in is always the named argument you do it with: " in => 'RGB' " just reads natural.
And just to complete bullet point 1: gradient can now take a series of colors and a tilt factor as arguments to produce very expressive and custom gradients. The tilt factor works also for complements. If you use special tilt values from the documentation you can get also split complementary colors as needed by designers but the nice thing about GTC is, you could choose any other value to get exactly what you are looking for. Many libraries have one method for triadic colors one for quadratic. To get them in GTC you just set the steps argument to 3 or 4 but you can choose again also any other number. Complements can be tilted in all 3 Dimensions.
Beside gradient and complement came also a new color set method: cluster. It is for computing a bunch of colors and are centered around a given one, but have a given, minimal dissimilarity. New is also invert, often the fastest way to get a fitting fore/background color, if the original color was not too bland.
The internal color name constants are still the same, but this feature block got 2 expansions. For one you can now ask for the closest color name (closest_name) and select from which standard this name has to come from (e.g. CSS). These Constants are provided by the Graphics::ColorNames::* modules and you can use them also anywhere a color is expected as input. The nice green from X11 standard would be just:'X:forestgreen'.
But since CSS + X11 + Pantone report colors are already included 'forestgreen' works too.
There are many more features that will come the next week, the most requested is probably simulation for color impaired vision, more spaces, a gamut checker is already implement, gamma correction, will be implemented this week and much much more. Just give it a try and please send bug reports and feature requests.
PS. Yes I also heald a lightning talk about GTC in Berlin last week.
PPS. 2.02 is out with gamma correction and correct complex color inversions in any space.
Modern software distribution has converged on a simple idea: ship a self-contained artifact. Whether that means a statically linked binary, a container image, or a snap/flatpak, the benefits are the same -- dependency management is solved at build time, platform differences are absorbed, and upgrades and rollbacks reduce to swapping a single file.
Perl's App::FatPacker
applies the same principle to Perl scripts. It bundles every pure-Perl
dependency into a single executable file. No cpanm, no
local::lib, no Makefile on the target -- just copy the file
and run it. The technique is well-established -- cpm (the
CPAN installer we use in the build) is itself distributed as a fatpacked
binary.
The distribution pipeline looks like this:
Code repo --> CI --> fatpack --> deploy --> laptops / jumpboxes / servers
|
single file,
no dependencies
This post walks through how we fatpacked an internal CLI we'll call
mycli, a ~90-module Perl app, into a single file. The
approach generalises to any App::Cmd-based tool.
A good practice for internal tools is to provide all three interfaces: a web frontend, an API, and a CLI. The web frontend is the easiest to discover; the API enables automation and integration; the CLI is the fastest path for engineers who live in a terminal. FatPacker makes the CLI trivially deployable.
mycli is a thin client -- it talks to an internal REST API
over HTTPS and renders the response locally. There is no local state beyond a config
file and environment variables. You could build an equivalent tool against
a binary RPC protocol such as gRPC or Thrift -- the fatpacking approach
is the same.
+--------------------+ +-------------------+
| Workstation | HTTPS | Server |
| | | |
| $ mycli resource |---------->| REST API ---+ |
| list ... |<----------| (JSON) DB | |
+--------------------+ +-------------------+
Despite being a thin client, mycli is not trivial.
It includes:
- Pluggable output renderers (table, JSON, YAML, CSV, plain text)
- Colour output with
NO_COLORsupport - Automatic pager integration (
less -RFX) and pipe/TTY detection - Activity spinner
- Multi-format ID resolution (numeric, UUID prefix, name lookup)
- Command aliases (
ls/list,get/show) - Config file discovery chain (env var, XDG path, dotfile)
- Timezone-aware timestamp rendering
- Structured syslog logging with per-invocation correlation IDs
- StatsD metrics instrumentation
- HTTP debugging hooks
All of this fatpacks cleanly because each feature is backed by pure-Perl modules.
This makes it an ideal fatpack candidate: the only XS dependency is
Net::SSLeay for TLS, which is typically already present on
the target system. Everything else is pure Perl.
Why FatPacker over PAR::Packer?
The other well-known option for single-file Perl distribution is
PAR::Packer. PAR
bundles everything -- including XS modules and even the perl
interpreter itself -- into a self-extracting archive. At runtime it
unpacks to a temp directory and executes from there.
FatPacker takes a different approach: modules are inlined as strings
inside the script and served via a custom @INC hook. There is
no extraction step, no temp directory, and no architecture coupling. The
trade-off is that FatPacker only handles pure Perl -- XS modules must
already be on the target.
For a thin REST client where the only XS dependency is
Net::SSLeay, FatPacker wins on simplicity: the output is a
plain Perl script, it starts instantly, and it runs on any architecture
with a compatible perl. PAR is the better choice when you
need to bundle XS-heavy dependencies or ship a binary to machines without
Perl at all.
What fatpacking does
FatPacker prepends a BEGIN block to your script containing
every dependency as a string literal, keyed by module path. A custom
@INC hook serves these strings to require
instead of reading from disk. The original script is appended
unchanged.
$ wc -l bin/mycli mycli-packed
13 bin/mycli
48721 mycli-packed
That ~49k line file runs identically to the original, on any machine with Perl 5.24+.
The problem with naive fatpacking
The standard FatPacker workflow is:
$ fatpack trace bin/mycli
$ fatpack packlists-for $(cat fatpacker.trace) > packlists
$ fatpack tree $(cat packlists)
$ fatpack file bin/mycli > mycli-packed
This breaks for non-trivial apps because fatpack trace
uses compile-time analysis (B::minus_c). It misses anything
loaded at runtime via require:
App::Cmddiscovers commands viaModule::Pluggableat runtimeText::ANSITableloads border styles and colour themes dynamicallyLWP::UserAgentloads protocol handlers on first requestYAML::Anyprobes for available backends at runtime
If the trace misses a module, the packed binary dies with
Can't locate Foo/Bar.pm in @INC at the worst possible moment.
The solution: a custom trace helper
Instead of relying on fatpack trace, we wrote a helper
script that requires every module the app could ever load,
then dumps %INC at exit. This captures the complete runtime
dependency tree.
#!/usr/bin/env perl
# bin/trace-helper -- not shipped, build-time only
use strict;
use warnings;
use lib 'lib';
# Modules loaded lazily that fatpack misses
require Data::Unixish::Apply;
require Digest::SHA;
require HTTP::Request;
require LWP::UserAgent;
require String::RewritePrefix;
# Exercise objects to trigger deep runtime loads
{
require Text::ANSITable;
my $t = Text::ANSITable->new(use_color => 1, use_utf8 => 1);
$t->border_style('UTF8::SingleLineBold');
$t->color_theme('Text::ANSITable::Standard::NoGradation');
$t->columns(['a']);
$t->add_row(['1']);
$t->draw; # forces all rendering deps to load
}
# Every App::Cmd leaf command
require MyCLI::App;
require MyCLI::App::Command::device::list;
require MyCLI::App::Command::device::get;
# ... all 80+ command modules ...
END {
open my $fh, '>', 'fatpacker.trace' or die $!;
for my $inc (sort keys %INC) {
next unless defined $INC{$inc};
next if $inc =~ m{\AMyCLI/}; # our own modules come from lib/
print $fh "$inc\n";
}
}
Key points:
- Don't call
->run--App::Cmdsubdispatch will die on duplicate command names across namespaces. Justrequireevery leaf. - Exercise both code paths --
Text::ANSITableloads different modules for colour vs plain, UTF-8 vs ASCII. Instantiate both. - Exclude your own namespace -- FatPacker embeds modules
from
fatlib/; yourlib/modules are embedded separately. Including them in the trace causes duplicates.
Forcing pure-Perl backends
FatPacker can only bundle pure Perl. Many popular modules ship dual
XS/pure-Perl backends and prefer XS at runtime. If XS is available during
the trace, the pure-Perl fallback won't appear in %INC and
won't get bundled.
Force pure-Perl mode during the build:
# In the fatpack build script
export B_HOOKS_ENDOFSCOPE_IMPLEMENTATION=PP
export LIST_MOREUTILS_PP=1
export MOO_XS_DISABLE=1
export PACKAGE_STASH_IMPLEMENTATION=PP
export PARAMS_VALIDATE_IMPLEMENTATION=PP
export PERL_JSON_BACKEND=JSON::PP
export PUREPERL_ONLY=1
PUREPERL_ONLY=1 is a
convention
respected by many dual XS/PP distributions at install time, preventing XS
compilation entirely. The per-module variables above cover modules that
don't check PUREPERL_ONLY.
Combine this with --pp at install time to avoid pulling in
XS at all:
cpm install -L local --target-perl 5.24.0 --pp
Pinning the target Perl version
The --target-perl flag to cpm is critical and
easy to overlook. Without it, cpm resolves dependency versions
against your build machine's Perl. If you're building on 5.38 but deploying
to a jumpbox running 5.24, you'll silently install module versions that use
postfix dereferencing, subroutine signatures, or
other features that don't exist on the target. The packed binary will fail
at runtime with a syntax error -- far from the build where you could catch
it.
This tells cpm's resolver to only consider module versions
whose metadata declares compatibility with 5.24.0. Combined with
perl -c as a post-install sanity check, this catches
version mismatches before the slow trace step.
The complete build script
Here is the full pipeline, wrapped in a shell script. It supports
incremental builds (reuses local/ and trace cache) and
--clean for full rebuilds.
#!/bin/sh
set -e
CLEAN=0
[ "$1" = "--clean" ] && CLEAN=1
# 0. Prerequisites
for cmd in cpm fatpack perl; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "Error: '$cmd' is not installed." >&2; exit 1
}
done
export PERL_USE_UNSAFE_INC=1 # Perl 5.26+ removed . from @INC
# 1. Install deps (pure-perl only)
if [ "$CLEAN" = 1 ] || [ ! -d local/ ]; then
rm -rf local/
cpm install -L local --target-perl 5.24.0 --pp
fi
# 2. Set up paths
export PERL5LIB=$PWD/lib:$PWD/local/lib/perl5
export PATH=$PWD/local/bin:$PATH
# 3. Force pure-perl backends
export B_HOOKS_ENDOFSCOPE_IMPLEMENTATION=PP
export LIST_MOREUTILS_PP=1
export MOO_XS_DISABLE=1
export PACKAGE_STASH_IMPLEMENTATION=PP
export PARAMS_VALIDATE_IMPLEMENTATION=PP
export PERL_JSON_BACKEND=JSON::PP
export PUREPERL_ONLY=1
# 4. Verify compilation
perl -c bin/mycli || exit 1
# 5. Trace
if [ "$CLEAN" = 1 ] || [ ! -f fatpacker.trace ]; then
perl -Ilib bin/trace-helper
echo "Trace: $(wc -l < fatpacker.trace) modules"
fi
# 6. Pack
fatpack packlists-for $(cat fatpacker.trace) > packlists
fatpack tree $(cat packlists)
# Strip arch-specific dirs and non-essential files
rm -rf fatlib/$(perl -MConfig -e 'print $Config{archname}')
find fatlib -name '*.pod' -delete
find fatlib -name '*.pl' -delete
# Bundle
fatpack file bin/mycli > mycli-packed
chmod +x mycli-packed
echo "Built mycli-packed ($(wc -c < mycli-packed) bytes)"
Step by step: what happens
- Prerequisites --
verify
cpm,fatpack, andperlare available - Install --
cpminstalls all dependencies intolocal/as pure Perl, targeting 5.24.0 - Paths and env --
set
PERL5LIB,PATH, and pure-Perl overrides - Compile check --
perl -c bin/myclicatches syntax errors before the slow trace step - Trace --
the helper script loads everything and writes the module list to
fatpacker.trace - Packlists and tree --
fatpack packlists-formaps module names to installed packlist files;fatpack treecopies the.pmfiles intofatlib/ - Clean up --
remove
.pod,.pl, and arch-specific directories to reduce size - Bundle --
fatpack fileinlines everything fromfatlib/into the script
Makefile integration
For teams that prefer make, add targets that delegate to the
shell script:
# In Makefile.PL, inside MY::postamble
.PHONY: pack clean_fatpack
pack:
./fatpack
clean :: clean_fatpack
clean_fatpack:
rm -rf fatlib fatpacker.trace packlists mycli-packed local/
Then building is just:
$ perl Makefile.PL
$ make pack
Adding a new dependency
When someone adds use Some::New::Module to the codebase,
the fatpacked binary will break with
Can't locate Some/New/Module.pm in @INC unless the build
picks it up. The workflow is:
- Add the module to
cpanfile - If the module is loaded at runtime (via
requireor a plugin mechanism), add arequire Some::New::Moduleline to the trace helper - Rebuild with
--clean
./fatpack --clean
The --clean flag is important. Without it, the build
reuses the cached local/ directory and
fatpacker.trace from the previous run. The new module won't
appear in either, and the packed binary will silently ship without it.
A good safeguard is to run perl -c mycli-packed after
every build -- this catches missing modules at build time rather than in
production.
What about perlstrip?
Perl::Strip can reduce the packed file by ~30% by removing comments,
POD, and whitespace from bundled modules. We deliberately left it off.
For an internal tool, the size saving (~1.7 MB) is not worth the
trade-off: stripped files are harder to debug with stack traces, and
perlstrip has a known issue corrupting files that contain
use utf8.
Gotchas and tips
XS modules cannot be fatpacked
Modules with C extensions (.so/.xs) cannot be
inlined. They must already exist on the target system. If your app has
many XS dependencies, consider PAR::Packer instead (see above).
PERL_USE_UNSAFE_INC
Perl 5.26 removed . from @INC. Some older
CPAN modules assume it's there during install or test. Set
PERL_USE_UNSAFE_INC=1 during the build to avoid spurious
failures. This only affects the build environment, not the packed
binary.
Pinto / private CPAN
If your organisation runs a private CPAN mirror (Pinto, OrePAN2, etc.),
point cpm at it with --resolver:
cpm install -L local --resolver 02packages,$PINTO_REPO --pp
Docker builds
FatPacker and Docker are complementary. Use Docker for the build environment (consistent Perl version, cpm, fatpack installed), and ship either the container image or just the packed file:
COPY mycli-packed /usr/local/bin/mycli
RUN chmod +x /usr/local/bin/mycli
Summary
The core recipe is three pieces:
- A trace helper that loads every module your app
could use at runtime, capturing the full dependency tree via
%INC - Pure-Perl enforcement via environment variables and
cpm --pp - The standard fatpack pipeline: packlists, tree, clean up, bundle
The result is a single file you can scp to any box with
Perl 5.24+ and run immediately. No CPAN, no Makefile, no containers
required.
References
- App::FatPacker on CPAN
- FatPacking Perl applications
-- talk by Andrew Rodland covering the core technique, pure-Perl
enforcement, and
cpm - arodland/swr fatpack script -- a clean, minimal reference implementation of the full pipeline
- App::cpm
-- fast CPAN installer (itself shipped as a fatpacked binary);
--target-perland--ppflags are essential for fatpack builds
Modern software distribution has converged on a simple idea: ship a self-contained artifact. Whether that means a statically linked binary, a container image, or a snap/flatpak, the benefits are the same -- dependency management is solved at build time, platform differences are absorbed, and upgrades and rollbacks reduce to swapping a single file.
Perl's App::FatPacker
applies the same principle to Perl scripts. It bundles every pure-Perl
dependency into a single executable file. No cpanm, no
local::lib, no Makefile on the target -- just copy the file
and run it. The technique is well-established -- cpm (the
CPAN installer we use in the build) is itself distributed as a fatpacked
binary.
The distribution pipeline looks like this:
Code repo --> CI --> fatpack --> deploy --> laptops / jumpboxes / servers
|
single file,
no dependencies
This post walks through how we fatpacked an internal CLI we'll call
mycli, a ~90-module Perl app, into a single file. The
approach generalises to any App::Cmd-based tool.
A good practice for internal tools is to provide all three interfaces: a web frontend, an API, and a CLI. The web frontend is the easiest to discover; the API enables automation and integration; the CLI is the fastest path for engineers who live in a terminal. FatPacker makes the CLI trivially deployable.
mycli is a thin client -- it talks to an internal REST API
over HTTPS and renders the response locally. There is no local state beyond a config
file and environment variables. You could build an equivalent tool against
a binary RPC protocol such as gRPC or Thrift -- the fatpacking approach
is the same.
+--------------------+ +-------------------+
| Workstation | HTTPS | Server |
| | | |
| $ mycli resource |---------->| REST API ---+ |
| list ... |<----------| (JSON) DB | |
+--------------------+ +-------------------+
Despite being a thin client, mycli is not trivial.
It includes:
- Pluggable output renderers (table, JSON, YAML, CSV, plain text)
- Colour output with
NO_COLORsupport - Automatic pager integration (
less -RFX) and pipe/TTY detection - Activity spinner
- Multi-format ID resolution (numeric, UUID prefix, name lookup)
- Command aliases (
ls/list,get/show) - Config file discovery chain (env var, XDG path, dotfile)
- Timezone-aware timestamp rendering
- Structured syslog logging with per-invocation correlation IDs
- StatsD metrics instrumentation
- HTTP debugging hooks
All of this fatpacks cleanly because each feature is backed by pure-Perl modules.
This makes it an ideal fatpack candidate: the only XS dependency is
Net::SSLeay for TLS, which is typically already present on
the target system. Everything else is pure Perl.
Why FatPacker over PAR::Packer?
The other well-known option for single-file Perl distribution is
PAR::Packer. PAR
bundles everything -- including XS modules and even the perl
interpreter itself -- into a self-extracting archive. At runtime it
unpacks to a temp directory and executes from there.
FatPacker takes a different approach: modules are inlined as strings
inside the script and served via a custom @INC hook. There is
no extraction step, no temp directory, and no architecture coupling. The
trade-off is that FatPacker only handles pure Perl -- XS modules must
already be on the target.
For a thin REST client where the only XS dependency is
Net::SSLeay, FatPacker wins on simplicity: the output is a
plain Perl script, it starts instantly, and it runs on any architecture
with a compatible perl. PAR is the better choice when you
need to bundle XS-heavy dependencies or ship a binary to machines without
Perl at all.
What fatpacking does
FatPacker prepends a BEGIN block to your script containing
every dependency as a string literal, keyed by module path. A custom
@INC hook serves these strings to require
instead of reading from disk. The original script is appended
unchanged.
$ wc -l bin/mycli mycli-packed
13 bin/mycli
48721 mycli-packed
That ~49k line file runs identically to the original, on any machine with Perl 5.24+.
The problem with naive fatpacking
The standard FatPacker workflow is:
$ fatpack trace bin/mycli
$ fatpack packlists-for $(cat fatpacker.trace) > packlists
$ fatpack tree $(cat packlists)
$ fatpack file bin/mycli > mycli-packed
This breaks for non-trivial apps because fatpack trace
uses compile-time analysis (B::minus_c). It misses anything
loaded at runtime via require:
App::Cmddiscovers commands viaModule::Pluggableat runtimeText::ANSITableloads border styles and colour themes dynamicallyLWP::UserAgentloads protocol handlers on first requestYAML::Anyprobes for available backends at runtime
If the trace misses a module, the packed binary dies with
Can't locate Foo/Bar.pm in @INC at the worst possible moment.
The solution: a custom trace helper
Instead of relying on fatpack trace, we wrote a helper
script that requires every module the app could ever load,
then dumps %INC at exit. This captures the complete runtime
dependency tree.
#!/usr/bin/env perl
# bin/trace-helper -- not shipped, build-time only
use strict;
use warnings;
use lib 'lib';
# Modules loaded lazily that fatpack misses
require Data::Unixish::Apply;
require Digest::SHA;
require HTTP::Request;
require LWP::UserAgent;
require String::RewritePrefix;
# Exercise objects to trigger deep runtime loads
{
require Text::ANSITable;
my $t = Text::ANSITable->new(use_color => 1, use_utf8 => 1);
$t->border_style('UTF8::SingleLineBold');
$t->color_theme('Text::ANSITable::Standard::NoGradation');
$t->columns(['a']);
$t->add_row(['1']);
$t->draw; # forces all rendering deps to load
}
# Every App::Cmd leaf command
require MyCLI::App;
require MyCLI::App::Command::device::list;
require MyCLI::App::Command::device::get;
# ... all 80+ command modules ...
END {
open my $fh, '>', 'fatpacker.trace' or die $!;
for my $inc (sort keys %INC) {
next unless defined $INC{$inc};
next if $inc =~ m{\AMyCLI/}; # our own modules come from lib/
print $fh "$inc\n";
}
}
Key points:
- Don't call
->run--App::Cmdsubdispatch will die on duplicate command names across namespaces. Justrequireevery leaf. - Exercise both code paths --
Text::ANSITableloads different modules for colour vs plain, UTF-8 vs ASCII. Instantiate both. - Exclude your own namespace -- FatPacker embeds modules
from
fatlib/; yourlib/modules are embedded separately. Including them in the trace causes duplicates.
Forcing pure-Perl backends
FatPacker can only bundle pure Perl. Many popular modules ship dual
XS/pure-Perl backends and prefer XS at runtime. If XS is available during
the trace, the pure-Perl fallback won't appear in %INC and
won't get bundled.
Force pure-Perl mode during the build:
# In the fatpack build script
export B_HOOKS_ENDOFSCOPE_IMPLEMENTATION=PP
export LIST_MOREUTILS_PP=1
export MOO_XS_DISABLE=1
export PACKAGE_STASH_IMPLEMENTATION=PP
export PARAMS_VALIDATE_IMPLEMENTATION=PP
export PERL_JSON_BACKEND=JSON::PP
export PUREPERL_ONLY=1
PUREPERL_ONLY=1 is a
convention
respected by many dual XS/PP distributions at install time, preventing XS
compilation entirely. The per-module variables above cover modules that
don't check PUREPERL_ONLY.
Combine this with --pp at install time to avoid pulling in
XS at all:
cpm install -L local --target-perl 5.24.0 --pp
Pinning the target Perl version
The --target-perl flag to cpm is critical and
easy to overlook. Without it, cpm resolves dependency versions
against your build machine's Perl. If you're building on 5.38 but deploying
to a jumpbox running 5.24, you'll silently install module versions that use
postfix dereferencing, subroutine signatures, or
other features that don't exist on the target. The packed binary will fail
at runtime with a syntax error -- far from the build where you could catch
it.
This tells cpm's resolver to only consider module versions
whose metadata declares compatibility with 5.24.0. Combined with
perl -c as a post-install sanity check, this catches
version mismatches before the slow trace step.
The complete build script
Here is the full pipeline, wrapped in a shell script. It supports
incremental builds (reuses local/ and trace cache) and
--clean for full rebuilds.
#!/bin/sh
set -e
CLEAN=0
[ "$1" = "--clean" ] && CLEAN=1
# 0. Prerequisites
for cmd in cpm fatpack perl; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "Error: '$cmd' is not installed." >&2; exit 1
}
done
export PERL_USE_UNSAFE_INC=1 # Perl 5.26+ removed . from @INC
# 1. Install deps (pure-perl only)
if [ "$CLEAN" = 1 ] || [ ! -d local/ ]; then
rm -rf local/
cpm install -L local --target-perl 5.24.0 --pp
fi
# 2. Set up paths
export PERL5LIB=$PWD/lib:$PWD/local/lib/perl5
export PATH=$PWD/local/bin:$PATH
# 3. Force pure-perl backends
export B_HOOKS_ENDOFSCOPE_IMPLEMENTATION=PP
export LIST_MOREUTILS_PP=1
export MOO_XS_DISABLE=1
export PACKAGE_STASH_IMPLEMENTATION=PP
export PARAMS_VALIDATE_IMPLEMENTATION=PP
export PERL_JSON_BACKEND=JSON::PP
export PUREPERL_ONLY=1
# 4. Verify compilation
perl -c bin/mycli || exit 1
# 5. Trace
if [ "$CLEAN" = 1 ] || [ ! -f fatpacker.trace ]; then
perl -Ilib bin/trace-helper
echo "Trace: $(wc -l < fatpacker.trace) modules"
fi
# 6. Pack
fatpack packlists-for $(cat fatpacker.trace) > packlists
fatpack tree $(cat packlists)
# Strip arch-specific dirs and non-essential files
rm -rf fatlib/$(perl -MConfig -e 'print $Config{archname}')
find fatlib -name '*.pod' -delete
find fatlib -name '*.pl' -delete
# Bundle
fatpack file bin/mycli > mycli-packed
chmod +x mycli-packed
echo "Built mycli-packed ($(wc -c < mycli-packed) bytes)"
Step by step: what happens
- 0Prerequisites --
verify
cpm,fatpack, andperlare available - 1Install --
cpminstalls all dependencies intolocal/as pure Perl, targeting 5.24.0 - 2Paths and env --
set
PERL5LIB,PATH, and pure-Perl overrides - 3Compile check --
perl -c bin/myclicatches syntax errors before the slow trace step - 4Trace --
the helper script loads everything and writes the module list to
fatpacker.trace - 5Packlists and tree --
fatpack packlists-formaps module names to installed packlist files;fatpack treecopies the.pmfiles intofatlib/ - 6Clean up --
remove
.pod,.pl, and arch-specific directories to reduce size - 7Bundle --
fatpack fileinlines everything fromfatlib/into the script
Makefile integration
For teams that prefer make, add targets that delegate to the
shell script:
# In Makefile.PL, inside MY::postamble
.PHONY: pack clean_fatpack
pack:
./fatpack
clean :: clean_fatpack
clean_fatpack:
rm -rf fatlib fatpacker.trace packlists mycli-packed local/
Then building is just:
$ perl Makefile.PL
$ make pack
Adding a new dependency
When someone adds use Some::New::Module to the codebase,
the fatpacked binary will break with
Can't locate Some/New/Module.pm in @INC unless the build
picks it up. The workflow is:
- Add the module to
cpanfile - If the module is loaded at runtime (via
requireor a plugin mechanism), add arequire Some::New::Moduleline to the trace helper - Rebuild with
--clean
./fatpack --clean
The --clean flag is important. Without it, the build
reuses the cached local/ directory and
fatpacker.trace from the previous run. The new module won't
appear in either, and the packed binary will silently ship without it.
A good safeguard is to run perl -c mycli-packed after
every build -- this catches missing modules at build time rather than in
production.
What about perlstrip?
Perl::Strip can reduce the packed file by ~30% by removing comments,
POD, and whitespace from bundled modules. We deliberately left it off.
For an internal tool, the size saving (~1.7 MB) is not worth the
trade-off: stripped files are harder to debug with stack traces, and
perlstrip has a known issue corrupting files that contain
use utf8.
Gotchas and tips
XS modules cannot be fatpacked
Modules with C extensions (.so/.xs) cannot be
inlined. They must already exist on the target system. If your app has
many XS dependencies, consider PAR::Packer instead (see above).
PERL_USE_UNSAFE_INC
Perl 5.26 removed . from @INC. Some older
CPAN modules assume it's there during install or test. Set
PERL_USE_UNSAFE_INC=1 during the build to avoid spurious
failures. This only affects the build environment, not the packed
binary.
Pinto / private CPAN
If your organisation runs a private CPAN mirror (Pinto, OrePAN2, etc.),
point cpm at it with --resolver:
cpm install -L local --resolver 02packages,$PINTO_REPO --pp
Docker builds
FatPacker and Docker are complementary. Use Docker for the build environment (consistent Perl version, cpm, fatpack installed), and ship either the container image or just the packed file:
COPY mycli-packed /usr/local/bin/mycli
RUN chmod +x /usr/local/bin/mycli
Summary
The core recipe is three pieces:
- A trace helper that loads every module your app
could use at runtime, capturing the full dependency tree via
%INC - Pure-Perl enforcement via environment variables and
cpm --pp - The standard fatpack pipeline: packlists, tree, clean up, bundle
The result is a single file you can scp to any box with
Perl 5.24+ and run immediately. No CPAN, no Makefile, no containers
required.
References
- App::FatPacker on CPAN
- FatPacking Perl applications
-- talk by Andrew Rodland covering the core technique, pure-Perl
enforcement, and
cpm - arodland/swr fatpack script -- a clean, minimal reference implementation of the full pipeline
- App::cpm
-- fast CPAN installer (itself shipped as a fatpacked binary);
--target-perland--ppflags are essential for fatpack builds
All three of us attended this long meeting covering quite a bit ground:
CVE-2026-3381 obliges us to cut a 5.42.2 point release with an updated Compress::Raw::Zlib.
We accepted Philippe’s and Eric’s offer to handle the last dev releases of the cycle.
Olaf Alders requested more explicit EOL notices and has updated
perlpolicy.podand the release manager guide accordingly. We agreed that the release announcement mails for the final dev release and the stable release should also contain a brief note about the perl version which is falling out of support, and filed an issue to make this happen.We sent mail to kick off the voting process for some new core team member candidates.
We discussed the state of Devel::PPPort. It has been outdated for some time and needs to be unstuck.
We would like to get
customize.datdown to the only entry that cannot be removed (forversion.pm). We will try to coordinate with maintainers.We noticed that we missed the deprecation of multiple
use VERSIONdeclarations in the same scope, which was supposed to be fatalized in 5.44. It is too late now to do that in this dev cycle, so the warning will have to change to 5.46 and the deprecation revisited next cycle.Further on the topic of overlooked deprecations, we considered how to prevent this from continuing to happen. We decided that some kind of documentation of recurring PSC obligations during a cycle is needed, which would also include things like the contentious changes freeze and release blocker triage.
There was not much time left for release blocker triage, so we only did a little, which surfaced no candidate blockers so far. (A few already-definite blockers have been spotted and marked outside of triage.)
The Board is pleased to share the 2025 Annual Report from the The Perl and Raku Foundation.
You can download the full report from the Perl and Raku Foundation website
Strengthening the Foundation
2025 was a year of both challenge and progress. Like many nonprofits, the Foundation faced funding constraints that required careful prioritization of resources. At the same time, increased focus on fundraising and donor engagement helped stabilize support for the work that matters most. A number of processes and tools were overhauled, allowing the Board to manage the funding more effectively, and pay grants more promptly and at lower overhead expense than had been the case previously.
Contributions from sponsors, corporate partners, and individual donors played a critical role in sustaining operations—particularly for core development and infrastructure.
Funding What Matters Most
Financial stewardship remained a top priority throughout the year. The Foundation focused its resources on:
- Supporting the Perl 5 Core Maintenance Fund
- Investing in Raku development and ecosystem improvements
- Maintaining essential infrastructure and services
While some grant activity was reduced during tighter periods, the report describes the Foundations recovery from those trials and outlines a clear path toward expanding funding as donations grow.
Our total income for the year was $253,744.86, with total expenditures of $233,739.75. 92% of our spending supported grants, events, and infrastructure. Our largest single expenditure remains the Perl Core Maintenance Grants, one of the long-time pillars of the Foundation's programs.
A Community-Funded Future
The Foundation’s work is made possible by the community it serves. Every donation—whether from individuals or organizations—directly supports the developers, tools, and systems that keep Perl and Raku reliable and evolving.
In 2025, we also strengthened our fundraising efforts, building a more sustainable base of recurring and long-term support to ensure continuity in the years ahead.
Looking Ahead
Our focus for the coming year is clear:
- Grow recurring donations and sponsorships
- Restore and expand the grants program
- Continue developing transparent, responsible financial management
We’re grateful to everyone who contributed in 2025. Your support keeps the ecosystem strong.
If you rely on Perl or Raku, we encourage you to take part in sustaining them. Your support is always welcome!
Every so often, a new data serialisation format appears and people get excited about it. Recently, one of those formats is **TOON** — Token-Oriented Object Notation. As the name suggests, it’s another way of representing the same kinds of data structures that you’d normally store in JSON or YAML: hashes, arrays, strings, numbers, booleans and nulls.
So the obvious Perl question is: *“Ok, where’s the CPAN module?”*
This post explains what TOON is, why some people think it’s useful, and why I decided to write a Perl module for it — with an interface that should feel very familiar to anyone who has used JSON.pm.
I should point out that I knew about [Data::Toon](https://metacpan.org/pod/Data::TOON) but I wanted something with an interface that was more like JSON.pm.
—
## What TOON Is
TOON stands for **Token-Oriented Object Notation**. It’s a textual format for representing structured data — the same data model as JSON:
* Objects (hashes)
* Arrays
* Strings
* Numbers
* Booleans
* Null
The idea behind TOON is that it is designed to be **easy for both humans and language models to read and write**. It tries to reduce punctuation noise and make the structure of data clearer.
If you think of the landscape like this:
| Format | Human-friendly | Machine-friendly | Very common |
| —— | ————– | —————- | ———– |
| JSON | Medium | Very | Yes |
| YAML | High | Medium | Yes |
| TOON | High | High | Not yet |
TOON is trying to sit in the middle: simpler than YAML, more readable than JSON.
Whether it succeeds at that is a matter of taste — but it’s an interesting idea.
—
## TOON vs JSON vs YAML
It’s probably easiest to understand TOON by comparing it to JSON and YAML. Here’s the same “person” record written in all three formats.
### JSON
{
“name”: “Arthur Dent”,
“age”: 42,
“email”: “arthur@example.com”,
“alive”: true,
“address”: {
“street”: “High Street”,
“city”: “Guildford”
},
“phones”: [
“01234 567890”,
“07700 900123”
]
}
### YAML
name: Arthur Dent
age: 42
email: arthur@example.com
alive: true
address:
street: High Street
city: Guildford
phones:
– 01234 567890
– 07700 900123
### TOON
name: Arthur Dent
age: 42
email: arthur@example.com
alive: true
address:
street: High Street
city: Guildford
phones[2]: 01234 567890,07700 900123
You can see that TOON sits somewhere between JSON and YAML:
* Less punctuation and quoting than JSON
* More explicit structure than YAML
* Still very easy to parse
* Still clearly structured for machines
That’s the idea, anyway.
—
## Why People Think TOON Is Useful
The current interest in TOON is largely driven by AI/LLM workflows.
People are using it because:
1. It is easier for humans to read than JSON.
2. It is less ambiguous and complex than YAML.
3. It maps cleanly to the JSON data model.
4. It is relatively easy to parse.
5. It works well in prompts and generated output.
In other words, it’s not trying to replace JSON for APIs, and it’s not trying to replace YAML for configuration files. It’s aiming at the space where humans and machines are collaborating on structured data.
You may or may not buy that argument — but it’s an interesting niche.
—
## Why I Wrote a Perl Module
I don’t have particularly strong opinions about TOON as a format. It might take off, it might not. We’ve seen plenty of “next big data format” ideas over the years.
But what I *do* have a strong opinion about is this:
> If a data format exists, then Perl should have a CPAN module for it that works the way Perl programmers expect.
Perl already has very good, very consistent interfaces for data serialisation:
* JSON
* YAML
* Storable
* Sereal
They all tend to follow the same pattern, particularly the object-oriented interface:
use JSON;
my $json = JSON->new->pretty->canonical;
my $text = $json->encode($data);
my $data = $json->decode($text);
So I wanted a TOON module that worked the same way.
—
## Design Goals
When designing the module, I had a few simple goals.
### 1. Familiar OO Interface
The primary interface should be object-oriented and feel like JSON.pm:
use TOON;
my $toon = TOON->new
->pretty
->canonical
->indent(2);
my $text = $toon->encode($data);
my $data = $toon->decode($text);
If you already know JSON, you already know how to use TOON.
There are also convenience functions, but the OO interface is the main one.
### 2. Pure Perl Implementation
Version 0.001 is pure Perl. That means:
* Easy to install
* No compiler required
* Works everywhere Perl works
If TOON becomes popular and performance matters, someone can always write an XS backend later.
### 3. Clean Separation of Components
Internally, the module is split into:
* **Tokenizer** – turns text into tokens
* **Parser** – turns tokens into Perl data structures
* **Emitter** – turns Perl data structures into TOON text
* **Error handling** – reports line/column errors cleanly
This makes it easier to test and maintain.
### 4. Do the Simple Things Well First
Version 0.001 supports:
* Scalars
* Arrayrefs
* Hashrefs
* undef → null
* Pretty printing
* Canonical key ordering
It does **not** (yet) try to serialise blessed objects or do anything clever. That can come later if people actually want it.
—
## Example Usage (OO Style)
Here’s a simple Perl data structure:
my $data = {
name => “Arthur Dent”,
age => 42,
drinks => [ “tea”, “coffee” ],
alive => 1,
};
### Encoding
use TOON;
my $toon = TOON->new->pretty->canonical;
my $text = $toon->encode($data);
print $text;
### Decoding
use TOON;
my $toon = TOON->new;
my $data = $toon->decode($text);
print $data->{name};
### Convenience Functions
use TOON qw(encode_toon decode_toon);
my $text = encode_toon($data);
my $data = decode_toon($text);
But the OO interface is where most of the flexibility lives.
—
## Command Line Tool
There’s also a command-line tool, toon_pp, similar to json_pp:
cat data.toon | toon_pp
Which will pretty-print TOON data.
—
## Final Thoughts
I don’t know whether TOON will become widely used. Predicting the success of data formats is a fool’s game. But the cost of supporting it in Perl is low, and the potential usefulness is high enough to make it worth doing.
And fundamentally, this is how CPAN has always worked:
> See a problem. Write a module. Upload it. See if anyone else finds it useful.
So now Perl has a TOON module. And if you already know how to use JSON.pm, you already know how to use it.
That was the goal.
The post Writing a TOON Module for Perl first appeared on Perl Hacks.
I am trying to understand the behavior of the following script under Perl 5.28.2:
sub split_and_print {
my $label = $_[0];
my $x = $_[1];
my @parts = split('\.', $x);
print sprintf("%s -> %s %s %.20f\n", $label, $parts[0], $parts[1], $x);
}
my @raw_values = ('253.38888888888889', '373.49999999999994');
for my $raw_value (@raw_values) {
split_and_print("'$raw_value'", $raw_value);
split_and_print("1.0 * '$raw_value'", 1.0 * $raw_value);
}
for me, this prints
'253.38888888888889' -> 253 38888888888889 253.38888888888888573092
1.0 * '253.38888888888889' -> 253 388888888889 253.38888888888888573092
'373.49999999999994' -> 373 49999999999994 373.49999999999994315658
1.0 * '373.49999999999994' -> 373 5 373.49999999999994315658
All of that is as expected, except for the last line: I don't understand why, during the automatic conversion of $x from a number to a string in the call to split it is converted into 373.5. print(373.49999999999994 - 373.5) says -5.6843418860808e-14, so Perl knows that those numbers are not equal (i.e. it's not about a limited precision of floating points in Perl).
perlnumber says
As mentioned earlier, Perl can store a number in any one of three formats, but most operators typically understand only one of those formats. When a numeric value is passed as an argument to such an operator, it will be converted to the format understood by the operator.
[...]
If the source number is outside of the limits representable in the target form, a representation of the closest limit is used. (Loss of information)
If the source number is between two numbers representable in the target form, a representation of one of these numbers is used. (Loss of information)
But '373.5' doesn't seem to be the "closest limit" of representing 373.49999999999994 as a string -- that would be '373.49999999999994', or some other decimal representation that, when converted back to a number yields the original value.
Also: what is different about 253.38888888888889?
I am looking for a definite reference that explains how exactly the automatic conversion of numbers to strings works in Perl.
-
Clone - recursively copy Perl datatypes
- Version: 0.50 on 2026-03-28, with 33 votes
- Previous CPAN version: 0.49 was released 3 days before
- Author: ATOOMIC
-
CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
- Version: 20260327.002 on 2026-03-27, with 25 votes
- Previous CPAN version: 20260318.001 was released 9 days before
- Author: BRIANDFOY
-
DBD::Oracle - Oracle database driver for the DBI module
- Version: 1.95 on 2026-03-24, with 33 votes
- Previous CPAN version: 1.91_5 was released 8 days before
- Author: ZARQUON
-
IPC::Run - system() and background procs w/ piping, redirs, ptys (Unix, Win32)
- Version: 20260322.0 on 2026-03-22, with 39 votes
- Previous CPAN version: 20250809.0 was released 7 months, 12 days before
- Author: TODDR
-
Mojo::Pg - Mojolicious ♥ PostgreSQL
- Version: 4.29 on 2026-03-23, with 98 votes
- Previous CPAN version: 4.28 was released 5 months, 23 days before
- Author: SRI
-
Object::Pad - a simple syntax for lexical field-based objects
- Version: 0.825 on 2026-03-25, with 48 votes
- Previous CPAN version: 0.824 was released 1 day before
- Author: PEVANS
-
PDL::Stats - a collection of statistics modules in Perl Data Language, with a quick-start guide for non-PDL people.
- Version: 0.856 on 2026-03-22, with 15 votes
- Previous CPAN version: 0.855 was released 1 year, 16 days before
- Author: ETJ
-
SPVM - The SPVM Language
- Version: 0.990152 on 2026-03-26, with 36 votes
- Previous CPAN version: 0.990151 was released the same day
- Author: KIMOTO
-
Term::Choose - Choose items from a list interactively.
- Version: 1.781 on 2026-03-25, with 15 votes
- Previous CPAN version: 1.780 was released 1 month, 20 days before
- Author: KUERBIS
-
YAML::Syck - Fast, lightweight YAML loader and dumper
- Version: 1.42 on 2026-03-27, with 18 votes
- Previous CPAN version: 1.41 was released 4 days before
- Author: TODDR
This is the weekly favourites list of CPAN distributions. Votes count: 43
Week's winner: Mail::Make (+2)
Build date: 2026/03/28 20:47:31 GMT
Clicked for first time:
- DB::Handy - Pure-Perl flat-file relational database with DBI-like interface
- GD::Thumbnail - Thumbnail maker for GD
- HTTP::Handy - A tiny HTTP/1.0 server for Perl 5.5.3+
- Lingua::IND::Nums2Words - This module is deprecated. Please use Lingua::IND::Num2Word instead.
- Lingua::ITA::Word2Num - Word to number conversion in Italian
- Lingua::KOR::Word2Num - Word to number conversion in Korean
- LTSV::LINQ - LINQ-style query interface for LTSV files
- MIDI::RtController::Filter::Tonal - Tonal RtController filters
- Modern::Perl::Prelude - Project prelude for modern Perl style on Perl 5.26+
- Net::Async::SOCKS - basic SOCKS5 connection support for IO::Async
- Restish::Client - A RESTish client...in perl!
Increasing its reputation:
- App::Greple (+1=5)
- Authen::SASL (+1=11)
- CGI (+1=48)
- CHI (+1=64)
- Data::Printer (+1=154)
- DateTime::Format::ISO8601 (+1=11)
- DBD::Pg (+1=104)
- DBI (+1=283)
- FFI::Platypus (+1=70)
- Future (+1=63)
- Future::AsyncAwait (+1=52)
- Imager::QRCode (+1=3)
- IO::Async (+1=81)
- IO::Async::SSL (+1=5)
- IO::Compress (+1=20)
- IO::K8s (+1=5)
- Lingua::JA::Moji (+1=3)
- List::UtilsBy (+1=41)
- Mail::Make (+2=2)
- mb::JSON (+1=3)
- Mojo::Pg (+1=74)
- Mojo::UserAgent::Cached (+1=4)
- Net::Async::HTTP (+1=8)
- Object::Pad (+1=48)
- PDF::API2 (+1=33)
- Regexp::Assemble (+1=36)
- Regexp::Debugger (+1=60)
- Syntax::Keyword::Match (+1=15)
- Syntax::Keyword::Try (+1=48)
- Test2::Harness (+1=21)
- XML::Parser (+1=11)
I just spend another fun and productive week in Marseille at the Koha Hackfest hosted by BibLibre. We (Mark, tadzik and me) arrived on Sunday (via plane from Vienna or Poland, and I came by train from Berlin via Strasbourg) and left on Friday.
There where the usual interesting discussions on all things Koha, presentations of new features and of course a lot of socializing. And cheese, so much cheese...
Elasticsearch
On the first day there was a discussion on Elasticsearch and getting rid of Zebra (the old search engine used by Koha). Actually getting rid of Zebra is not an option (now), because small installation won't want to set up and run Elasticsearch. But Mark proposed using our Marc Normalization Plugin as the basis for a new internal, DB-only search engine (so no need for an external index etc) and over the course of the week (and with LLM help) implemented a prototype. It would really be amazing if we could get this running!
I worked a bit on improving Elasticsearch indexing:
- Bulk biblio ES index update after auth change: When merging (or updating) authorities, the Elasticsearch indexing of the linked biblios now will happen in one background job per authority instead of one background job per biblio. So an authority that is used in 100 biblios will now trigger one indexing background job with 100 biblio items instead of 100 background jobs with 1 biblio item each.
- Zebraqueue should not be added to when only Elasticsearch is used: We added a new syspref "ElasticsearchEnableZebraQueue". If disabled, no data will be written to the zebraqueue table, because usually when using Elasticsearch you don't need to also run Zebra.
I got sign-offs and Pass-QA for both issues during the hackfest, thanks Fridolin, Paul and Baptiste (who owns the coolest tea mug at BibLibre..)
QA
I also did QA on a bunch of other issues: 22639, 35267, 36550, 39158, 40906, 41767, 41967, 42107. Some of them where of interest to me, some I did because other people nicely asked me to :-)
LLM, "AI" and Agentic Coding
This was again a hot topic, with some people using those tools to great effect, some hating them, and some in between. As in my last post on the German Perl Workshop I again want to point out this blog post: I Sold Out for $20 a Month and All I Got Was This Perfectly Generated Terraform, and during the event the post Thoughts on slowing the fuck down dropped (by Mario Zechner, who wrote the coding agent I (currently) use).
Anyway, Koha now has some guidelines on AI and LLM-assisted contributions and on using LLM features inside Koha.
Claude vs domm
While working on unit tests for Bug 40577 I struggled with a test failing only if I run the whole test script (as opposed to only the one subtest I was working on). It seemed to be a problem with mocked tests, so I asked Joubu (who was by chance just standing next to me). Together we figured out the scoping problem: If you use Test::MockObject/MockModule multiple times on a class from different scopes, the mocked methods/functions might not automatically be removed. You have to call unmock explicitly. After the patch was done, I described the error to Claude and asked for a fix, expecting to not get anything useable. But (to my slight horror) it produced the correct explanation and fix in very short time. On the one hand: amazing; on the other hand: very scary.
Other random stuff:
- When it rains and a TGV arrives at the station, more people have the idea to take a taxi than taxis are available. So walking the short distance was necessary, but we (Katrin, who I met on the train, and me) still got wet. At least we had cold burgers...
- Paul showed me a non-Koha tool he has written: mdv - A terminal markdown viewer with vim keybindings. Very nice, I especially like it to view checkouts of gitlab wikis!
- I was not the only Team Scheisse fan attending! Philip++
- Philip also pointed out the very detailed and interesting shared notes produced by various attendees during the event.
- At my third visit to Marseille, I manage to navigate the city center quite well.
- I finally made it to the Tangerine record store, very nice selection. I still did not let the shop owner talk me into buying a 200€ original UK pressing of Unknown Pleasures by Joy Division.
- I did not get Moule Frits, but at least some Galette and Cidre.
- After being to Senegal in February, I now realized that there are a lot of places selling Yassa and Mafe in Marseille. I guess they where there last year too, I just did not see them, having never eaten Yassa or Mafe before.
- It can get very windy in Marseille.
- I should do it like Jake(?) and cycle (at least partly) to the next hackfest.
Thanks
Thanks to BibLibre and Paul Poulain for organizing the event, and to all the attendees for making it such a wonderful 5 days!
Looking forward to meet you all again at the upcoming KohaCon in Karlsruhe
Updates
- 2026-03-03: Added link to shared notes.
Make, Bash, and a scripting language of your choice
Creating AWS Resources…let me count the ways
You need to create an S3 bucket, an SQS queue, an IAM policy and a few other AWS resources. But how?…TIMTOWTDI
The Console
- Pros: visual, immediate feedback, no tooling required, great for exploration
- Cons: not repeatable, not version controllable, opaque, clickops doesn’t scale, “I swear I configured it the same way”
The AWS CLI
- Pros: scriptable, composable, already installed, good for one-offs
- Cons: not idempotent by default, no state management, error handling is manual, scripts can grow into monsters
CloudFormation
- Pros: native AWS, state managed by AWS, rollback support, drift detection
- Cons: YAML/JSON verbosity, slow feedback loop, stack update failures are painful, error messages are famously cryptic, proprietary to AWS, subject to change without notice
Terraform
- Pros: multi-cloud, huge community, mature ecosystem, state management, plan before apply
- Cons: state file complexity, backend configuration, provider versioning, HCL is yet another language to learn, overkill for small projects, often requires tricks & contortions
Pulumi
- Pros: real programming languages, familiar abstractions, state management
- Cons: even more complex than Terraform, another runtime to install and maintain
CDK
- Pros: real programming languages, generates CloudFormation, good for large organizations
- Cons: CloudFormation underneath means CloudFormation problems, Node.js dependency
…and the rest of crew…
Ansible, AWS SAM, Serverless Framework - each with their own opinions, dependencies, and learning curves.
Every option beyond the CLI adds a layer of abstraction, a new language or DSL, a state management story, and a new thing to learn and maintain. For large teams managing hundreds of resources across multiple environments that overhead is justified. For a solo developer or small team managing a focused set of resources it can feel like overkill.
Even in large organizations, not every project should be conflated into the corporate infrastructure IaC tool. Moreover, not every project gets the attention of the DevOps team necessary to create or support the application infrastructure.
What if you could get idempotent, repeatable, version-controlled
infrastructure management using tools you already have? No new
language, no state backend, no provider versioning. Just make,
bash, a scripting language you’re comfortable with, and your cloud
provider’s CLI.
And yes…my love affair with make is endless.
We’ll use AWS examples throughout, but the patterns apply equally to
Google Cloud (gcloud) and Microsoft Azure (az). The CLI tools
differ, the patterns don’t.
A word about the AWS CLI --query option
Before you reach for jq, perl, or python to parse CLI output,
it’s worth knowing that most cloud CLIs have built-in query
support. The AWS CLI’s --query flag implements JMESPath - a query
language for JSON that handles the majority of filtering and
extraction tasks without any additional tools:
# get a specific field
aws lambda get-function \
--function-name my-function \
--query 'Configuration.FunctionArn' \
--output text
# filter a list
aws sqs list-queues \
--query 'QueueUrls[?contains(@, `my-queue`)]|[0]' \
--output text
--query is faster, requires no additional dependencies, and keeps
your pipeline simple. Reach for it first. When it falls short -
complex transformations, arithmetic, multi-value extraction - that’s
when a one-liner earns its place:
# perl
aws lambda get-function --function-name my-function | \
perl -MJSON -n0 -e '$l=decode_json($_); print $l->{Configuration}{FunctionArn}'
# python
aws lambda get-function --function-name my-function | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d['Configuration']['FunctionArn'])"
Both get the job done. Use whichever lives in your shed.
What is Idempotency?
The word comes from mathematics - an operation is idempotent if applying it multiple times produces the same result as applying it once. Sort of like those ID10T errors…no matter how hard or how many times that user clicks on that button they get the same result.
In the context of infrastructure management it means this: running your resource creation script twice should have exactly the same outcome as running it once. The first run creates the resource. The second run detects it already exists and does nothing - no errors, no duplicates, no side effects.
This sounds simple but it’s surprisingly easy to get wrong. A naive
script that just calls aws lambda create-function will fail on the
second run with a ResourceConflictException. A slightly better
script wraps that in error handling. A truly idempotent script never
attempts to create a resource it knows already exists.
And it works in both directions. The idempotent bug - running a failing process repeatedly and getting the same error every time - is what happens when your failure path is idempotent too. Consistently wrong, no matter how many times you try. The patterns we’ll show are designed to ensure that success is idempotent while failure always leaves the door open for the next attempt.
Cloud APIs fall into four distinct behavioral categories when it comes to idempotency, and your tooling needs to handle each one differently:
Case 1 - The API is idempotent and produces output
Some APIs can be called repeatedly without error and return useful
output each time - whether the resource was just created or already
existed. aws events put-rule is a good example - it returns the rule
ARN whether the rule was just created or already existed. The pattern:
call the read API first, capture the output, call the write API only
if the read returned nothing.
Case 2 - The API is idempotent but produces no output
Some write APIs succeed silently - they return nothing on
success. aws s3api put-bucket-notification-configuration is a good
example. It will happily overwrite an existing configuration without
complaint, but returns no output to confirm success. The pattern: call
the API, synthesize a value for your sentinel using && echo to
capture something meaningful on success.
Case 3 - The API is not idempotent
Some APIs will fail with an error if you try to create a resource that
already exists. aws lambda add-permission returns
ResourceConflictException if the statement ID already exists. aws
lambda create-function returns ResourceConflictException if the
function already exists. These APIs give you no choice - you must
query first and only call the write API if the resource is missing.
Case 4 - The API call fails
Any of the above can fail - network errors, permission problems,
invalid parameters. When a call fails you must not leave behind a
sentinel file that signals success. A stale sentinel is worse than no
sentinel - it tells Make the resource exists when it doesn’t, and
subsequent runs silently skip the creation step. The patterns: || rm
-f $@ when writing directly, or else rm -f $@ when capturing to a
variable first.
The Sentinel File
Before we look at the four patterns in detail, we need to introduce a concept that ties everything together: the sentinel file.
A sentinel file is simply a file whose existence signals that a task
has been completed successfully. It contains no magic - it might hold
the output of the API call that created the resource, or it might just
be an empty file created with touch. What matters is that it exists
when the task succeeded and doesn’t exist when it hasn’t.
make has used this pattern since the 1970s. When you declare a
target in a Makefile, make checks whether a file with that name
exists before deciding whether to run the recipe. If the file exists
and is newer than its dependencies, make skips the recipe
entirely. If the file doesn’t exist, make runs the recipe to create
it.
For infrastructure management this is exactly the behavior we want:
my-resource:
@value="$$(aws some-service describe-resource \
--name $(RESOURCE_NAME) 2>&1)"; \
if [[ -z "$$value" || "$$value" = "ResourceNotFound" ]]; then \
value="$$(aws some-service create-resource \
--name $(RESOURCE_NAME))"; \
fi; \
test -e $@ || echo "$$value" > $@
The first time you run make my-resource the file doesn’t exist,
the recipe runs, the resource is created, and the API response
is written to the sentinel file my-resource. The second time you
run it, make sees the file exists and skips the recipe entirely -
zero API calls.
When an API call fails we want to be sure we do not create the sentinel file. We’ll cover the failure case in more detail in Pattern 4 of the next section.
The Four Patterns
Armed with the sentinel file concept and an understanding of the four API behavioral categories, let’s look at concrete implementations of each pattern.
Pattern 1 - Idempotent API with output
The simplest case. Query the resource first - if it exists capture the output and write the sentinel. If it doesn’t exist, create it, capture the output, and write the sentinel. Either way you end up with a sentinel containing meaningful content.
The SQS queue creation is a good example:
sqs-queue:
@queue="$$(aws sqs list-queues \
--query 'QueueUrls[?contains(@, `$(QUEUE_NAME)`)]|[0]' \
--output text --profile $(AWS_PROFILE) 2>&1)"; \
if echo "$$queue" | grep -q 'error\|Error'; then \
echo "ERROR: list-queues failed: $$queue" >&2; \
exit 1; \
elif [[ -z "$$queue" || "$$queue" = "None" ]]; then \
queue="$(QUEUE_NAME)"; \
aws sqs create-queue --queue-name $(QUEUE_NAME) \
--profile $(AWS_PROFILE); \
fi; \
test -e $@ || echo "$$queue" > $@
Notice --query doing the filtering work before the output reaches
the shell. No jq, no pipeline - the AWS CLI extracts exactly what we
need. The result is either a queue URL or empty. If empty we
create. Either way $$queue ends up with a value and the sentinel is
written exactly once.
The EventBridge rule follows the same pattern:
lambda-eventbridge-rule:
@rule="$$(aws events describe-rule \
--name $(RULE_NAME) \
--profile $(AWS_PROFILE) 2>&1)"; \
if echo "$$rule" | grep -q 'ResourceNotFoundException'; then \
rule="$$(aws events put-rule \
--name $(RULE_NAME) \
--schedule-expression "$(SCHEDULE_EXPRESSION)" \
--state ENABLED \
--profile $(AWS_PROFILE))"; \
elif echo "$$rule" | grep -q 'error\|Error'; then \
echo "ERROR: describe-rule failed: $$rule" >&2; \
exit 1; \
fi; \
test -e $@ || echo "$$rule" > $@
Same shape - query, create if missing, write sentinel once.
Pattern 2 - Idempotent API with no output
Some APIs succeed silently. aws s3api
put-bucket-notification-configuration is the canonical example - it
happily overwrites an existing configuration and returns nothing. No
output means nothing to write to the sentinel.
The solution is to synthesize a value using &&:
define notification_configuration =
use JSON;
my $lambda_function = $ENV{lambda_function};
my $function_arn = decode_json($lambda_function)->{Configuration}->{FunctionArn};
my $configuration = {
LambdaFunctionConfigurations => [ {
LambdaFunctionArn => $function_arn,
Events => [ split ' ', $ENV{s3_event} ],
}
]
};
print encode_json($configuration);
endef
export s_notification_configuration = $(value notification_configuration)
lambda-s3-trigger: lambda-s3-permission
temp="$$(mktemp)"; trap 'rm -f "$$temp"' EXIT; \
lambda_function="$$(cat lambda-function)"; \
echo $$(s3_event="$(S3_EVENT)" lambda_function="$$lambda_function" \
perl -e "$$s_notification_configuration") > $$temp; \
trigger="$$(aws s3api put-bucket-notification-configuration \
--bucket $(BUCKET_NAME) \
--notification-configuration file://$$temp \
--profile $(AWS_PROFILE) && cat $$temp)"; \
test -e $@ || echo "$$trigger" > $@
The && cat $$temp is the key. If the API call succeeds the &&
fires and $$trigger gets the configuration JSON string - something meaningful to
write to the sentinel. If the API call fails && doesn’t fire,
$$trigger stays empty because the Makefile recipe aborts.
Using a
scriptlet (s_notification_configuration)
might seem like overkill, but it’s worth not having to fight shell
quoting issues!
Writing JSON used in many AWS API calls to a temporary file is usually a better way than passing a string on the command line. Unless you wrap the JSON in quotes you’ll be fighting shell quoting and interpolation issues…and of course you can write your scriptlets in Perl or Python!
Pattern 3 - Non-idempotent API
Some APIs are not idempotent - they fail with a
ResourceConflictException or similar if the resource already
exists. aws lambda add-permission and aws lambda create-function
are both in this category. There is no “create or update” variant -
you must check existence first and only call the write API if the
resource is missing.
The Lambda S3 permission target is a good example:
lambda-s3-permission: lambda-function s3-bucket
@permission="$$(aws lambda get-policy \
--function-name $(FUNCTION_NAME) \
--profile $(AWS_PROFILE) 2>&1)"; \
if echo "$$permission" | grep -q 'ResourceNotFoundException' || \
! echo "$$permission" | grep -q s3.amazonaws.com; then \
permission="$$(aws lambda add-permission \
--function-name $(FUNCTION_NAME) \
--statement-id s3-trigger-$(BUCKET_NAME) \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::$(BUCKET_NAME) \
--profile $(AWS_PROFILE))"; \
elif echo "$$permission" | grep -q 'error\|Error'; then \
echo "ERROR: get-policy failed: $$permission" >&2; \
exit 1; \
fi; \
if [[ -n "$$permission" ]]; then \
test -e $@ || echo "$$permission" > $@; \
else \
rm -f $@; \
fi
A few things worth noting here…
get-policyreturns the full policy document which may contain multiple statements - we check for the presence ofs3.amazonaws.comspecifically using! grep -qrather than just checking for an empty response. This handles the case where a policy exists but doesn’t yet have the S3 permission we need.- The sentinel is only written if
$$permissionis non-empty after the if block. This covers the case whereget-policyreturns nothing andadd-permissionalso fails - the sentinel stays absent and the nextmakerun will try again. - We pipe errors to our
bashvariable to detect the case where the resource does not exist or there may have been some other error. When other failures are possible2>&1combined with specific error string matching gives you both idempotency and visibility. Swallowing errors silently (2>/dev/null) is how idempotent bugs are born.
Pattern 4 - Failure handling
This isn’t a separate pattern so much as a discipline that applies to all three of the above. There are two mechanisms depending on how the sentinel is written.
Case 1: When the sentinel is written directly by the command:
aws lambda create-function ... > $@ || rm -f $@
|| rm -f $@ ensures that if the command fails the partial or empty
sentinel is immediately cleaned up. Without it make sees the file on
the next run and silently skips the recipe - an idempotent bug.
Case 2: When the sentinel is written by capturing output to a variable first:
if [[ -n "$$value" ]]; then \
test -e $@ || echo "$$value" > $@; \
else \
rm -f $@; \
fi
The else rm -f $@ serves the same purpose. If the variable is empty
- because the API call failed - the sentinel is removed. If the
sentinel doesn’t exist yet nothing is written. Either way the next
make run will try again.
In both cases the goal is the same: a sentinel file should only exist when the underlying resource exists. A stale sentinel is worse than no sentinel.
Depending on the way your recipe is written you may not need to test
the variable that capture the output at all. In Makefiles we
.SHELLFLAGS := -ec which causes make to exit immediately if any
command in a recipe fails. This means targets that don’t write to
$@ - like our sqs-queue target above
- don’t need explicit failure handling. make will die loudly and the
sentinel won’t be written. In that case you don’t even need to test
$$value and can simplify writing of the sentinel file like this:
test -e $@ || echo "$$value" > $@
Conclusion
Creating AWS resources can be done using several different tools…all of them eventually call AWS APIs and process the return payloads. Each of these tools has its place. Each adds something. Each also has a complexity, dependencies, and a learning curve score.
For a small project or a focused set of resources - the kind a solo
developer or small team manages for a specific application - you don’t
need tools with a high cognitive or resource load. You can use those
tools you already have on your belt; make,bash, [insert favorite
scripting language here], and aws. And you can leverage those same tools
equally well with gcloud or az.
The four patterns we’ve covered handle every AWS API behavior you’ll encounter:
- Query first, create only if missing, write a sentinel
- Synthesize output when the API has none
- Always check before calling a non-idempotent API
- Clean up on failure with
|| rm -f $@
These aren’t new tricks - they’re straightforward applications of
tools that have been around for decades. make has been managing
file-based dependencies since 1976. The sentinel file pattern predates
cloud computing entirely. We’re just applying them to a new problem.
One final thought. The idempotent bug - running a failing process
repeatedly and getting the same error every time - is the mirror image
of what we’ve built here. Our goal is idempotent success: run it once,
it works. Run it again, it still works. Run it a hundred times,
nothing changes. || rm -f $@ is what separates idempotent success
from idempotent failure - it ensures that a bad run always leaves the
door open for the next attempt rather than cementing the failure in
place with a stale sentinel.
Your shed is already well stocked. Sometimes the right tool for the job is the one you’ve had hanging on the wall for thirty years.
Further Reading
- “Advanced Bash-Scripting Guide” - https://tldp.org/LDP/abs/html/index.html
- “GNU Make” - https://www.gnu.org/software/make/manual/html_node/index.html
- Dave Oswald, “Perl One Liners for the Shell” (Perl conference presentation): https://www.slideshare.net/slideshow/perl-oneliners/77841913
- Peteris Krumins, “Perl One-Liners” (No Starch Press): https://nostarch.com/perloneliners
- Sundeep Agarwal, “Perl One-Liners Guide” (free online): https://learnbyexample.github.io/learn_perl_oneliners/
- AWS CLI JMESPath query documentation: https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-filter.html
I'm currently in a train from Berlin to Strasbourg and then onward to Marseille, traveling from the 28th(!) German Perl Workshop to the Koha Hackfest. I spend a few days after the Perl Workshop in Berlin with friends from school who moved to Berlin during/after university, hanging around at their homes and neighborhoods, visiting museums, professional industrial kitchens and other nice and foody places. But I want to review the Perl Workshop, so:
German Perl Workshop
It seems the last time I've attended a German Perl Workshop was in 2020 (literally days before the world shut down...), so I've missed a bunch of nice events and possibilities to meet up with old Perl friends. But even after this longish break it felt a bit like returning home :-)
I traveled to Berlin by sleeper train (worked without a problem) arriving on Monday morning a few hours before the workshop started. I went to a friends place (where I'm staying for the week), dumped my stuff, got a bike, and did a nice morning cycle through Tiergarten to the venue. Which was an actual church! And not even a secularized one.
Day 1
After a short introduction and welcome by Max Maischein (starting with a "Willkommen, liebe Gemeinde" fitting the location) he started the workshop with a talk on Claude Code and Coding-Agents. I only recently started to play around a bit with similar tools, so I could related to a lot of the topics mentioned. And I (again?) need to point out the blog post I Sold Out for $20 a Month and All I Got Was This Perfectly Generated Terraform which sums up my feelings and experiences with LLMs much better than I could.
Abigail then shared a nice story on how they (Booking.com) sharded a database, twice using some "interesting" tricks to move the data around and still getting reads from the correct replicas, all with nearly no downtime. Fun, but as "my" projects usually operate on a much smaller scale than Booking I will probably not try to recreate their solution.
For lunch I met with Michael at a nearby market hall for some Vietnamese food to do some planing for the upcoming Perl Toolchain Summit in Vienna.
Lars Dieckow then talked about data types in databases, or actually the lack of more complex types in databases and how one could still implement such types in SQL. Looks interesting, but probably a bit to hackish for me to actually use. I guess I have to continue handling such cases in code (which of course feels ugly, especially as I've learned to move more and more code into the DB using CTEs and window functions).
Next Flavio S. Glock showed his very impressive progress with PerlOnJava, a Perl distribution for the JVM. Cool, but probably not something I will use (mostly because I don't run Java anywhere, so adding it to our stack would make things more complex).
Then Lars showed us some of his beloved tools in Aus dem Nähkästchen, continuing a tradition started by Sven Guckes (RIP). I am already using some of the tools (realias, fzf, zoxide, htop, ripgrep) but now plan to finally clean up my dotfiles using xdg-ninja.
Now it was time for my first talk at this workshop, on Using class, the new-ish feature available in Perl (since 5.38) for native keywords for object-oriented programming. I also sneaked in some bibliographic data structures (MAB2 and MARCXML) to share my pain with the attendees. I was a tiny bit (more) nervous, as this was the first time I was using my current laptop (a Framework running Sway/Wayland) with an external projector, but wl-present worked like a charm. After the talk Wolfram Schneider showed me his MAB2->MARC online converter, which could maybe have been a basis for our tool, but then writing our own was a "fun" way to learn about MAB2.
The last talk of the day was Lee Johnson with I Bought A Scanner showing us how he got an old (ancient?) high-res foto scanner working again to scan his various film projects. Fun and interesting!
Between the end of the talks and the social event I went for some coffee with Paul Cochrane, and we where joined by Sawyer X and Flavio and some vegan tiramisu. Paul and me then cycled to the Indian restaurant through some light drizzle and along the Spree, and only then I realized that Paul cycled all the way from Hannover to Berlin. I was a bit envious (even though I in fact did cycle to Berlin 16 years ago (oh my, so long ago..)). Dinner was nice, but I did not stay too long.
Day 2
Tuesday started with Richard Jelinek first showing us his rather impressive off-grid house (or "A technocrat's house - 2050s standard") and the software used to automate it before moving on the the actual topic of his talk, Perl mit AI which turned out to be about a Perl implementation in Rust called pperl developed with massive LLM support. Which seems to be rather fast. As with PerlOnJava, I'm not sure I really want to use an alternative implementation (and of course currently pperl is marked as "Research Preview — WORK IN PROGRESS — please do not use in production environments") but maybe I will give it a try when it's more stable. Especially since we now have containers, which make setting up some experimental environments much easier.
Then Alexander Thurow shared his Thoughts on (Modern?) Software Development, lots of inspirational (or depressing) quotes and some LLM criticism lacking at the workshop (until now..)
Next up was Lars (again) with a talk on Hierarchien in SQL where we did a very nice derivation on how to get from some handcrafted SQL to recursive CTEs to query hierarchical graph data (DAG). I used (and even talked about) recursive CTEs a few times, but this was by far the best explanation I've ever seen. And we got to see some geizhals internals :-)
Sören Laird Sörries informed us on Digitale Souveränität und Made in Europe and I'm quite proud to say that I'm already using a lot of the services he showed (mailbox, Hetzner, fairphone, ..) though we could still do better (eg one project is still using a bunch of Google services)
Then Salve J. Nilsen (whose name I will promise to not mangle anymore) showed us his thoughts on What might a CPAN Steward organization look like?. We already talked about this topic a few weeks ago (in preparation of the Perl Toolchain Summit), so I was not paying a lot of attention (and instead hacked up a few short slides for a lightning talk) - Sorry. But in the discussion afterwards Salve clarified that the Cyber Resilience Act applies to all "CE-marked products" and that even a Perl API backend that power a mobile app running on a smartphone count as "CE-marked products". Before that I was under the assumption that only software running on actual physical products need the attestation. So we should really get this Steward organization going and hopefully even profit from it!
The last slot of the day was filled with the Lightning Talks hosted by R Geoffrey Avery and his gong. I submitted two and got a "double domm" slot, where I hurried through my microblog pipeline (on POSSE and getting not-twitter-tweets from my command line via some gitolite to my self hosted microblog and the on to Mastodon) followed by taking up Lars' challenge to show stuff from my own "Nähkästchen", in my case gopass and tofi (and some bash pipes) for an easy password manager.
We had the usual mixture of fun and/or informative short talks, but the highlight for me was Sebastian Gamaga, who did his first talk at a Perl event on How I learned about the problem differentiating a Hash from a HashRef. Good slides, well executed and showing a problem that I'm quite sure everybody encountered when first learning Perl (and I have to admit I also sometimes mix up hash/ref and regular/curly-braces when setting up a hash). Looking forward for a "proper" talk by Sebastian next year :-)
This evening I skipped having dinner with the Perl people, because I had to finish some slides for Wednesday and wanted to hang out with my non-Perl friends. But I've heard that a bunch of people had fun bouldering!
Day 3
I had a job call at 10:00 and (unfortunately) a bug to fix, so I missed the three talks in the morning session and only arrived at the venue during lunch break and in time for Paul Cochrane talking about Getting FIT in Perl (and fit he did get, too!). I've only recently started to collect exercise data (as I got a sport watch for my birthday) and being able to extract and analyze the data using my own software is indeed something I plan to do.
Next up was Julien Fiegehenn on Turning humans into SysAdmins, where he showed us how he used LLMs to adapt his developer mentorship framework to also work for sysadmin and getting them (LLMs, not fresh Sysadmins) to differentiate between Julian and Julien (among other things..)
For the final talk it was my turn again: Deploying Perl apps using Podman, make & gitlab. I'm not too happy with slides, as I had to rush a bit to finish them and did not properly highlight all the important points. But it still went well (enough) and it seemed that a few people found one of the main points (using bash / make in gitlab CI instead of specifying all the steps directly in .gitlab-ci.yml) useful.
Then Max spoke the closing words and announced the location of next years German Perl Workshop, which will take place in Hannover! Nice, I've never been there and plan to attend (and maybe join Paul on a bike ride there?)
Summary
As usual, a lot of thanks to the sponsors, the speakers, the orgas and the attendees. Thanks for making this nice event possible!
-
App::cpanminus - get, unpack, build and install modules from CPAN
- Version: 1.7049 on 2026-03-17, with 286 votes
- Previous CPAN version: 1.7048 was 1 year, 4 months, 18 days before
- Author: MIYAGAWA
-
App::HTTPThis - Export the current directory over HTTP
- Version: v0.11.1 on 2026-03-16, with 25 votes
- Previous CPAN version: v0.11.0 was 2 days before
- Author: DAVECROSS
-
CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
- Version: 20260318.001 on 2026-03-18, with 25 votes
- Previous CPAN version: 20260315.002 was 3 days before
- Author: BRIANDFOY
-
Crypt::Passphrase - A module for managing passwords in a cryptographically agile manner
- Version: 0.022 on 2026-03-21, with 17 votes
- Previous CPAN version: 0.021 was 1 year, 1 month, 17 days before
- Author: LEONT
-
DBD::Pg - DBI PostgreSQL interface
- Version: 3.20.0 on 2026-03-19, with 103 votes
- Previous CPAN version: 3.19.0 was 4 days before
- Author: TURNSTEP
-
Git::CPAN::Patch - Patch CPAN modules using Git
- Version: 2.5.2 on 2026-03-18, with 45 votes
- Previous CPAN version: 2.5.1
- Author: YANICK
-
JSON - JSON (JavaScript Object Notation) encoder/decoder
- Version: 4.11 on 2026-03-22, with 109 votes
- Previous CPAN version: 4.10 was 3 years, 5 months, 13 days before
- Author: ISHIGAKI
-
JSON::PP - JSON::XS compatible pure-Perl module.
- Version: 4.18 on 2026-03-20, with 22 votes
- Previous CPAN version: 4.17_01 was 2 years, 7 months, 21 days before
- Author: ISHIGAKI
-
Log::Any - Bringing loggers and listeners together
- Version: 1.719 on 2026-03-16, with 69 votes
- Previous CPAN version: 1.718 was 9 months, 14 days before
- Author: PREACTION
-
MetaCPAN::API - (DEPRECATED) A comprehensive, DWIM-featured API to MetaCPAN
- Version: 0.52 on 2026-03-16, with 26 votes
- Previous CPAN version: 0.51 was 8 years, 9 months, 9 days before
- Author: HAARG
-
Module::CoreList - what modules shipped with versions of perl
- Version: 5.20260320 on 2026-03-20, with 44 votes
- Previous CPAN version: 5.20260308 was 11 days before
- Author: BINGOS
-
Net::SSLeay - Perl bindings for OpenSSL and LibreSSL
- Version: 1.96 on 2026-03-21, with 27 votes
- Previous CPAN version: 1.95_03
- Author: CHRISN
-
OpenGL - Perl bindings to the OpenGL API, GLU, and GLUT/FreeGLUT
- Version: 0.7009 on 2026-03-19, with 15 votes
- Previous CPAN version: 0.7008
- Author: ETJ
-
SPVM - The SPVM Language
- Version: 0.990150 on 2026-03-19, with 36 votes
- Previous CPAN version: 0.990149
- Author: KIMOTO
-
Syntax::Construct - Explicitly state which non-feature constructs are used in the code.
- Version: 1.045 on 2026-03-19, with 14 votes
- Previous CPAN version: 1.044 was 10 days before
- Author: CHOROBA
-
TimeDate - Date and time formatting subroutines
- Version: 2.35 on 2026-03-21, with 28 votes
- Previous CPAN version: 2.34_03 was 1 day before
- Author: ATOOMIC
-
Unicode::UTF8 - Encoding and decoding of UTF-8 encoding form
- Version: 0.70 on 2026-03-19, with 20 votes
- Previous CPAN version: 0.69
- Author: CHANSEN
-
YAML::Syck - Fast, lightweight YAML loader and dumper
- Version: 1.39 on 2026-03-21, with 18 votes
- Previous CPAN version: 1.38
- Author: TODDR

We are re-opening the talk submissions with a new deadline of April 21, 2026. Please submit your 20 minute talks, and 50 minute talks at https://tprc.us/. Let us know if you need help with your submission or your talk development, because we have mentors who can listen to your ideas and guide you.
We are also taking submissions for interactive sessions. These are sessions that have a theme, but invite maximum audience participation; sessions which take advantage of the gathering of community members that have a wide range of experience and ideas to share. You would introduce the theme and moderate the session. If you have ideas for interactive sessions, but don’t want to moderate them yourself, please go to our wiki to enter your ideas, and maybe someone else will pick up the ball!
About eighteen months ago, I wrote a post called On the Bleading Edge about my decision to start using Perl’s new class feature in real code. I knew I was getting ahead of parts of the ecosystem. I knew there would be occasional pain. I decided the benefits were worth it.
I still think that’s true.
But every now and then, the bleading edge reminds you why it’s called that.
Recently, I lost a couple of days to a bug that turned out not to be in my code, not in the module I was installing, and not even in the module that module depended on — but in the installer’s understanding of modern Perl syntax.
This is the story.
The Symptom
I was building a Docker image for Aphra. As part of the build, I needed to install App::HTTPThis, which depends on Plack::App::DirectoryIndex, which depends on WebServer::DirIndex.
The Docker build failed with this error:
#13 45.66 --> Working on WebServer::DirIndex #13 45.66 Fetching https://www.cpan.org/authors/id/D/DA/DAVECROSS/WebServer-DirIndex-0.1.3.tar.gz ... OK #13 45.83 Configuring WebServer-DirIndex-v0.1.3 ... OK #13 46.21 Building WebServer-DirIndex-v0.1.3 ... OK #13 46.75 Successfully installed WebServer-DirIndex-v0.1.3 #13 46.84 ! Installing the dependencies failed: Installed version (undef) of WebServer::DirIndex is not in range 'v0.1.0' #13 46.84 ! Bailing out the installation for Plack-App-DirectoryIndex-v0.2.1.
Now, that’s a deeply confusing error message.
It clearly says that WebServer::DirIndex was successfully installed. And then immediately says that the installed version is undef and not in the required range.
At this point you start wondering if you’ve somehow broken version numbering, or if there’s a packaging error, or if the dependency chain is wrong.
But the version number in WebServer::DirIndex was fine. The module built. The tests passed. Everything looked normal.
So why did the installer think the version was undef?
When This Bug Appears
This only shows up in a fairly specific situation:
- A module uses modern Perl
classsyntax - The module defines a
$VERSION - Another module declares a prerequisite with a specific version requirement
- The installer tries to check the installed version without loading the module
- It uses Module::Metadata to extract
$VERSION - And the version of Module::Metadata it is using doesn’t properly understand
class
If you don’t specify a version requirement, you’ll probably never see this. Which is why I hadn’t seen it before. I don’t often pin minimum versions of my own modules, but in this case, the modules are more tightly coupled than I’d like, and specific versions are required.
So this bug only appears when you combine:
modern Perl syntax + version checks + older toolchain
Which is pretty much the definition of “bleading edge”.
The Real Culprit
The problem turned out to be an older version of Module::Metadata that had been fatpacked into cpanm.
cpanm uses Module::Metadata to inspect modules and extract $VERSION without loading the module. But the older Module::Metadata didn’t correctly understand the class keyword, so it couldn’t work out which package the $VERSION belonged to.
So when it checked the installed version, it found… nothing.
Hence:
Installed version (undef) of WebServer::DirIndex is not in range ‘v0.1.0’
The version wasn’t wrong. The installer just couldn’t see it.
An aside, you may find it amusing to hear an anecdote from my attempts to debug this problem.
I spun up a new Ubuntu Docker container, installed cpanm and tried to install Plack::App::DirectoryIndex. Initially, this gave the same error message. At least the problem was easily reproducible.
I then ran code that was very similar to the code cpanm uses to work out what a module’s version is.
$ perl -MModule::Metadata -E'say Module::Metadata->new_from_module("WebServer::DirIndex")->version'This displayed an empty string. I was really onto something here. Module::Metadata couldn’t find the version.
I was using Module::Metadata version 1.000037 and, looking at the change log on CPAN, I saw this:
1.000038 2023-04-28 11:25:40Z-detects "class" syntax
$ perl -MModule::Metadata -E'say Module::Metadata->new_from_module("WebServer::DirIndex")->version'
0.1.3That seemed conclusive. Excitedly, I reran the Docker build.
It failed again.
You’ve probably worked out why. But it took me a frustrating half an hour to work it out.
cpanm doesn’t use the installed version of Module::Metadata. It uses its own, fatpacked version. Updating Module::Metadata wouldn’t fix my problem.
The Workaround
I found a workaround. That was to add a redundant package declaration alongside the class declaration, so older versions of Module::Metadata can still identify the package that owns $VERSION.
So instead of just this:
class WebServer::DirIndex {
our $VERSION = '0.1.3';
...
}I now have this:
package WebServer::DirIndex;
class WebServer::DirIndex {
our $VERSION = '0.1.3';
...
}It looks unnecessary. And in a perfect world, it would be unnecessary.
But it allows older tooling to work out the version correctly, and everything installs cleanly again.
The Proper Fix
Of course, the real fix was to update the toolchain.
So I raised an issue against App::cpanminus, pointing out that the fatpacked Module::Metadata was too old to cope properly with modules that use class.
Tatsuhiko Miyagawa responded very quickly, and a new release of cpanm appeared with an updated version of Module::Metadata.
This is one of the nice things about the Perl ecosystem. Sometimes you report a problem and the right person fixes it almost immediately.
When Do I Remove the Workaround?
This leaves me with an interesting question.
The correct fix is “use a recent cpanm”.
But the workaround is “add a redundant package line so older tooling doesn’t get confused”.
So when do I remove the workaround?
The answer is probably: not yet.
Because although a fixed cpanm exists, that doesn’t mean everyone is using it. Old Docker base images, CI environments, bootstrap scripts, and long-lived servers can all have surprisingly ancient versions of cpanm lurking in them.
And the workaround is harmless. It just offends my sense of neatness slightly.
So for now, the redundant package line stays. Not because modern Perl needs it, but because parts of the world around modern Perl are still catching up.
Life on the Bleading Edge
This is what life on the bleading edge actually looks like.
Not dramatic crashes. Not language bugs. Not catastrophic failures.
Just a tool, somewhere in the install chain, that looks at perfectly valid modern Perl code and quietly decides that your module doesn’t have a version number.
And then you lose two days proving that you are not, in fact, going mad.
But I’m still using class. And I’m still happy I am.
You just have to keep an eye on the whole toolchain — not just the language — when you decide to live a little closer to the future than everyone else.
The post Still on the [b]leading edge first appeared on Perl Hacks.
Abstract
Even if you’re skeptical about AI writing your code, you’re leaving time on the table.
Many developers have been slow to adopt AI in their workflows, and that’s understandable. As AI coding assistants become more capable the anxiety is real - nobody wants to feel like they’re training their replacement. But we’re not there yet. Skilled developers who understand logic, mathematics, business needs and user experience will be essential to guide application development for the foreseeable future.
The smarter play is to let AI handle the parts of the job you never liked anyway - the documentation, the release notes, the boilerplate tests - while you stay focused on the work that actually requires your experience and judgment. You don’t need to go all in on day one. Here are six places to start.
1. Unit Test Writing
Writing unit tests is one of those tasks most developers know they should do more of and few enjoy doing. It’s methodical, time-consuming, and the worst time to write them is when the code reviewer asks if they pass.
TDD is a fine theory. In practice, writing tests before you’ve vetted your design means rewriting your tests every time the design evolves - which is often. Most experienced developers write tests after the design has settled, and that’s a perfectly reasonable approach.
The important thing is that they get written at all. Even a test that
simply validates use_ok(qw(Foo::Bar)) puts scaffolding in place that
can be expanded when new features are added or behavior changes. A
placeholder is infinitely more useful than nothing.
This is where AI earns its keep. Feed it a function or a module and it will identify the code paths that need coverage - the happy path, the edge cases, the boundary conditions, the error handling. It will suggest appropriate test data sets including the inputs most likely to expose bugs: empty strings, nulls, negative numbers, off-by-one values - the things a tired developer skips.
You review it, adjust it, own it. AI did the mechanical work of thinking through the permutations. You make sure it reflects how your code is actually used in the real world.
2. Documentation
“Documentation is like sex: when it’s good, it’s very, very good; and when it’s bad, it’s better than nothing.” - said someone somewhere.
Of course, there are developers that justify their disdain for writing documentation with one of two arguments (or both):
- The code is the documentation
- Documentation is wrong the moment it is written
It is true, the single source of truth regarding what code actually does is the code itself. What it is supposed to do is what documentation should be all about. When they diverge it’s either a defect in the software or a misunderstanding of the business requirement captured in the documentation.
Code that changes rapidly is difficult to document, but the intent of the code is not. Especially now with AI. It is trivial to ask AI to review the current documentation and align it with the code, negating point #2.
Feed AI a module and ask it to generate POD. It will describe what the code does. Your job is to verify that what it does is what it should do - which is a much faster review than writing from scratch.
3. Release Notes
If you’ve read this far you may have noticed the irony - this post was written by someone who just published a blog post about automating release notes with AI. So consider this section field-tested.
Release notes sit at the intersection of everything developers dislike: writing prose, summarizing work they’ve already mentally moved on from, and doing it with enough clarity that non-developers can understand what changed and why it matters. It’s the last thing standing between you and shipping.
The problem with feeding a git log to AI is that git logs are written for developers in the moment, not for readers after the fact. “Fix the thing” and “WIP” are not useful release note fodder.
The better approach is to give AI real context - a unified diff, a file manifest, and the actual source of the changed files. With those three inputs AI can identify the primary themes of a release, group related changes, and produce structured notes that actually reflect the architecture rather than just the line changes.
A simple make release-notes target can generate all three assets
automatically from your last git tag. Upload them, prompt for your
preferred format, and you have a first draft in seconds rather than
thirty minutes. Here’s how I built
it.
You still edit it. You add color, context, and the business rationale that only you know. But the mechanical work of reading every diff and turning it into coherent prose? Delegated.
4. Bug Triage
Debugging can be the most frustrating and the most rewarding experience for a developer. Most developers are predisposed to love a puzzle and there is nothing more puzzling than a race condition or a dangling pointer. Even though books and posters have been written about debugging it is sometimes difficult to know exactly where to start.
Describe the symptoms, share the relevant code, toss your theory at it. AI will validate or repudiate without ego - no colleague awkwardly telling you you’re wrong. It will suggest where to look, what telemetry to add, and before you know it you’re instrumenting the code that should have been instrumented from the start.
AI may not find your bug, but it will be a fantastic bug buddy.
5. Code Review
Since I’ve started using AI I’ve found that one of the most valuable things I can do with it is to give it my first draft of a piece of code. Anything more than a dozen or so lines is fair game.
Don’t waste your time polishing a piece of lava that just spewed from your noggin. There’s probably some gold in there and there’s definitely some ash. That’s ok. You created the framework for a discussion on design and implementation. Before you know it you have settled on a path.
AI’s strength is pattern recognition. It will recognize when your code needs to adopt a different pattern or when you nailed it. Get feedback. Push back. It’s not a one-way conversation. Question the approach, flag the inconsistencies that don’t feel right - your input into that review process is critical in evolving the molten rock into a solid foundation.
6. Legacy Code Deciphering
What defines “Legacy Code?” It’s a great question and hard to answer. And not to get too racy again, but as it has been said of pornography, I can’t exactly define it but I know it when I see it.
Fortunately (and yes I do mean fortunately) I have been involved in maintaining legacy code since the day I started working for a family run business in 1998. The code I maintained there was born literally in the late 70’s and still, to this day generates millions of dollars. You will never learn more about coding than by maintaining legacy code.
These are the major characteristics of legacy code from my experience (in order of visibility):
- It generates so much money for a company they could not possibly think of it being unavailable.
- It is monolithic and may in fact consist of modules in multiple languages.
- It is grown organically over the decades.
- It is more than 10 years old.
- The business rules are not documented, opaque and can only be discerned by a careful reading of the software. Product managers and users think they know what the software does, but probably do not have the entire picture.
- It cannot easily be re-written (by humans) because of #5.
- It contains as much dead code that is no longer serving any useful purpose as it does useful code.
I once maintained a C program that searched an ISAM database of legal judgments. The code had been ported from a proprietary in-memory binary tree implementation and was likely older than most of the developers reading this post. The business model was straightforward and terrifying - miss a judgment and we indemnify the client. Every change had to be essentially idempotent. You weren’t fixing code, you were performing surgery on a patient who would sue you if the scar was in the wrong place.
I was fortunate - there were no paydays for a client on my watch. But I wish I’d had AI back then. Not to write the code. To help me read it.
Now, where does AI come in? Points 5, 6, and definitely 7.
Throw a jabberwocky of a function at AI and ask it what it does. Not what it should do - what it actually does. The variable names are cryptic, the comments are either missing or lying, and the original author left the company during the Clinton administration. AI doesn’t care. It reads the code without preconception and gives you a plain English explanation of the logic, the assumptions baked in, and the side effects you never knew existed.
That explanation becomes your documentation. Those assumptions become your unit tests. Those side effects become the bug reports you never filed because you didn’t know they were bugs.
Dead code is where AI particularly shines. Show it a module and ask what’s unreachable. Ask what’s duplicated. Ask what hasn’t been touched in a decade but sits there quietly terrifying anyone who considers deleting it. AI will give you a map of the minefield so you can walk through it rather than around it forever.
Along the way AI will flag security vulnerabilities you never knew were there - input validation gaps, unsafe string handling, authentication assumptions that made sense in 1998 and are a liability today. It will also suggest where instrumentation is missing, the logging and telemetry that would have made every debugging session for the last twenty years shorter. You can’t go back and add it to history, but you can add it now before the next incident.
The irony of legacy code is that the skills required to understand it - patience, pattern recognition, the ability to hold an entire system in your head - are exactly the skills AI complements rather than replaces. You still need to understand the business. AI just helps you read the hieroglyphics.
Conclusion
None of the six items on this list require you to hand over the keys. You are still the architect, the decision maker, the person who understands the business and the user. AI is the tireless assistant who handles the parts of the job that drain your energy without advancing your craft.
The developers who thrive in the next decade won’t be the ones who resisted AI the longest. They’ll be the ones who figured out earliest how to delegate the tedious, the mechanical, and the repetitive - and spent the time they saved on the work that actually requires a human.
You don’t have to go all in. Start with a unit test. Paste some legacy code and ask AI to explain it or document it. Think of AI as that senior developer you go to with the tough problems - the one who has seen everything, judges nothing, and is available at 3am when the production system is on fire.
Only this one never sighs when you knock on the door.
Answer
You can configure grub via several ways to use a specific kernel or you can configure grub to use the latest one, or you can tell grub to pick one from a selection.
One specific kernel
If you inspect /etc/grub/grub.cfg you’ll see entries like this:
# the \ are mine, these are usually one big line but for blog purposes I
# multilined them
menuentry 'Debian GNU/Linux GNU/Linux, with Linux 6.12.8-amd64' --class debian \
--class gnu-linux --class gnu --class os $menuentry_id_option \
'gnulinux-6.12.8-amd64-advanced-5522bbcf-dc03-4d36-a3fe-2902be938ed4' {
You can use two identifiers to configure grub; you can use 'Debian GNU/Linux GNU/Linux, with Linux 6.12.8-amd64' or you can use the $menuentry_id_option
with gnulinux-6.12.8-amd64-advanced-5522bbcf-dc03-4d36-a3fe-2902be938ed4.
The Problem: Generating Release Notes is Boring
You’ve just finished a marathon refactoring - perhaps splitting a monolithic script into proper modules-and now you need to write the release notes. You could feed an AI a messy git log, but if you want high-fidelity summaries that actually understand your architecture, you need to provide better context.
The Solution: AI Loves Boring Tasks
…and is pretty good at them too!
Instead of manually describing changes or hoping it can interpret my ChangeLog, I’ve automated the production of three ephemeral “Sidecar” assets. These are generated on the fly, uploaded to the LLM, and then purged after analysis - no storage required.
The Assets
- The Manifest (
.lst): A simple list of every file touched, ensuring the AI knows the exact scope of the release. - The Logic (
.diffs): A unified diff (usinggit diff --no-ext-diff) that provides the “what” and “why” of every code change. - The Context (
.tar.gz): This is the “secret sauce.” It contains the full source of the changed files, allowing the AI to see the final implementation - not just the delta.
The Makefile Implementation
If you’ve read any of my blog
posts you
know I’m a huge Makefile fan. To automate this I’m naturally going
to add a recipe to my Makefile or Makefile.am.
First, we explicitly set the shell to /usr/bin/env bash to ensure features
like brace expansion work consistently across all dev environments.
# Ensure a portable bash environment for advanced shell features
SHELL := /usr/bin/env bash
.PHONY: release-notes clean-local
# Default to the version file, but allow command-line overrides
VERSION ?= $(shell cat VERSION)
release-notes:
@curr_ver=$(VERSION); \
last_tag=$$(git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1); \
diffs="release-$$curr_ver.diffs"; \
diff_list="release-$$curr_ver.lst"; \
diff_tarball="release-$$curr_ver.tar.gz"; \
echo "Comparing $$last_tag to current $$curr_ver..."; \
git diff --no-ext-diff "$$last_tag" "$$curr_ver" > "$$diffs"; \
git diff --name-only --diff-filter=AMR "$$last_tag" "$$curr_ver" > "$$diff_list"; \
tar -cf - -T "$$diff_list" --transform "s|^|release-$$curr_ver/|" | gzip > "$$diff_tarball"; \
ls -alrt release-$$curr_ver*
clean-local:
@echo "Cleaning ephemeral release assets..."
rm -f release-*.{tar.gz,lst,diffs}
Breaking Down the Recipe
- The Shell Choice (
/usr/bin/env bash): We avoid hardcoding paths to ensure the script finds the correct Bash path on macOS, Linux, or inside a container. - The Version Override (
VERSION ?=): This allows the “pre-flight” trick: runningmake release-notes VERSION=HEADto iterate on notes before you’ve actually tagged the release. - Smart Tag Discovery (
--sort=-v:refname): Usingv:refnameforces Git to use semantic versioning logic (so1.10.0correctly follows1.2.0), while the glob pattern filters out “noisy” non-version tags. - The Diff Filter (
--diff-filter=AMR): This ensures the tarball only includes files that actually exist (Added, Modified, or Renamed). If a release deleted a file, this filter preventstarfrom erroring out when it can’t find the missing file on disk. - The Cleanup Crew (
clean-local): remove the ephemeral artifacts usingbashexpansion
The AI Prompt
Once your assets are generated, upload them to AI and use a prompt that enforces your specific formatting standards.
The Prompt:
I have uploaded a file manifest (`.lst`), a unified diff
(`.diffs`), and a tarball of the source code (`.tar.gz`). Please
analyze these to identify the primary themes of this
release. Generate a release entry using this protocol:
1. Separate versions with 71 dashes.
2. Include a high-level executive summary paragraph.
3. Indent bullet points exactly 21 spaces to maintain a vertical column.
4. Group changes under category labels like `Core Bedrock:` or `Miniserver:`.
5. Provide plain-text ASCII only inside a code block.
The Result
In a recent dry run for Bedrock 3.8.8, this exact workflow allowed the
AI to correctly identify that bedrock-miniserver.pl.in was
refactored into Bedrock::MiniServer and Bedrock::MiniServer::CLI,
while accurately noting that Bedrock::Error moved to the
Bedrock-Core distribution.
In actuality, version 3.8.8 focuses on the primary goals of MiniServer maturation (more batteries included), Apache decoupling, and distribution hygiene. Here’s AI’s version:
-----------------------------------------------------------------------
Bedrock 3.8.8
-----------------------------------------------------------------------
This release focuses on the maturation of the MiniServer component,
decoupling core Bedrock logic from Apache-specific dependencies, and
improving distribution hygiene. Key architectural changes include
refactoring the MiniServer into modular components and moving
exception handling into the Bedrock-Core distribution.
2026-03-17 - 3.8.8 - MiniServer Maturation and Apache Decoupling
Miniserver:
- Refactored bedrock-miniserver.pl into modular
Bedrock::MiniServer and Bedrock::MiniServer::CLI.
- Implemented zero-config scaffolding to
automatically create application trees.
- Integrated full Bedrock configuration pipeline
for parity with Apache environments.
- Updated bedrock_server_config to support both
getter and setter operations.
Core:
- Moved Bedrock::Error and Bedrock::Exception to
the Bedrock-Core distribution.
- Introduced Bedrock::FauxHandler as a production-
ready alias for test handlers.
- Added dist_dir() to BLM::Startup::Bedrock to
expose distribution paths to templates.
Fixes:
- Demoted Apache-specific modules (mod_perl2,
Apache2::Request) to optional recommendations.
- Improved Bedrock::Test::FauxHandler to handle
caller-supplied loggers and safe destruction.
Conclusion
As I mentioned in a response to a recent Medium article, AI can be an accelerator for seasoned professionals. You’re not cheating. You did the work. AI does the wordsmithing. You edit, add color, and ship. What used to take 30 minutes now takes 3. Now that’s working smarter, not harder!
Pro-Tip
Add this to the top of your Makefile
SHELL := /usr/bin/env bash
# Default to the version file, but allow command-line overrides
VERSION ?= $(shell cat VERSION)
Copy this to a file named release-notes.mk
.PHONY: release-notes clean-local
release-notes:
@curr_ver=$(VERSION); \
last_tag=$$(git tag -l '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1); \
diffs="release-$$curr_ver.diffs"; \
diff_list="release-$$curr_ver.lst"; \
diff_tarball="release-$$curr_ver.tar.gz"; \
echo "Comparing $$last_tag to current $$curr_ver..."; \
git diff --no-ext-diff "$$last_tag" "$$curr_ver" > "$$diffs"; \
git diff --name-only --diff-filter=AMR "$$last_tag" "$$curr_ver" > "$$diff_list"; \
tar -cf - -T "$$diff_list" --transform "s|^|release-$$curr_ver/|" | gzip > "$$diff_tarball"; \
ls -alrt release-$$curr_ver*
clean-local:
@echo "Cleaning ephemeral release assets..."
rm -f release-*.{tar.gz,lst,diffs}
Then add release-notes.mk to your Makefile
include release-notes.mk

Dave writes:
Last month I worked on various miscellaneous issues, including a few performance and deparsing regressions.
Summary: * 3:00 GH #24110 ExtUtils::ParseXS after 5.51 prevents some XS modules to build * 2:49 GH# #24212 goto void XSUB in scalar context crashes * 7:19 XS: avoid core distros using void ST(0) hack * 2:40 fix up Deparse breakage * 5:41 remove OP_NULLs in OP_COND execution path
Total: * 21:29 (HH::MM)

Paul writes:
Not too much activity of my own this month here, as I spent a lot of Perl time working on other things like magic-v2 or some CPAN module ecosystem like Future::IO. Plus I had a stage show to finish building props for and manage the running of.
But I did manage to do:
- 3 = Continue work on attributes-v2 and write a provisional PR for the first stage
- https://github.com/Perl/perl5/pull/24171
- 3 = Bugfix in class.c in threaded builds
- https://github.com/Perl/perl5/issues/24150
- https://github.com/Perl/perl5/pull/24171
- 1 = More
foreachlvref neatening- https://github.com/Perl/perl5/pull/24202
- 3 = Various github code reviews
Total: 10 hours
Now that both attributes-v2 and magic-v2 are parked awaiting the start of the 5.45.x development cycle, most of my time until then will be spent on building up some more exciting features to launch those with, as well as continuing to focus on fixing any release-blocker bugs for 5.44.

Tony writes:
``` [Hours] [Activity] 2026/02/02 Monday 0.08 #24122 review updates and comment 0.17 #24063 review updates and apply to blead 0.28 #24062 approve with comment and bonus comment 0.92 #24071 review updates and approve 0.40 #24080 review updates, research and comment 0.18 #24122 review updates and approve 0.27 #24157 look into it and original ticket, comment on original ticket 0.58 #24134 review and comments 0.27 #24144 review and approve with comment 0.18 #24155 review and comment 0.48 #16865 debugging
0.90 #16865 debugging, start a bisect with a better test case
4.71
2026/02/03 Tuesday 0.17 review steve’s suggested maint-votes and vote 0.17 #24155 review updates and approve 1.30 #24073 recheck, comments and apply to blead 0.87 #24082 more review, follow-ups 0.83 #24105 work on threads support
0.65 #24105 more work on threads, hash randomization support
3.99
2026/02/04 Wednesday 0.13 github notifications 1.92 #24163 review, comments 0.48 #24105 rebase some more, fix tests, do a commit and push for CI (needs more work)
1.70 #24105 more cleanup and push for CI
4.23
2026/02/05 Thursday 0.20 github notifications 0.38 #24105 review CI results and fix some issues 1.75 #24082 research and comments 0.63 #24105 more CI results, update the various generated config files and push for CI 0.17 #23561 review updates and comment 0.40 #24163 research and follow-up
0.58 #24098 review updates and comments
4.11
2026/02/09 Monday 0.15 #24082 comment 0.20 #22040 comment 0.30 #24005 research, comment 0.33 #4106 rebase again and apply to blead 0.35 #24133 comment 0.35 #24168 review CI results and comment 0.25 #24098 comment 0.18 #24129 review updates and comment 0.92 #24160 review, comment, approve 0.17 #24136 review and briefly comment 0.78 #24179 review, comments
0.48 #16865 comment, try an approach
4.46
2026/02/10 Tuesday 0.62 #24163 comment 0.23 #24082 research
0.20 #24082 more research
1.05
2026/02/11 Wednesday 0.48 #24163 review updates and approve 0.73 #24129 review updates 0.45 #24098 research and follow-up comment 0.32 #24134 review updates and approve 0.17 #24080 review updates and approve 1.18 #22132 setup, testing and comments on ticket and upstream llvm ticket 0.32 #23561 review update and approve 0.42 #24179 review some more and make a suggestion
1.03 #24187 review and comments
5.10
2026/02/12 Thursday 0.43 #24136 research and comment 0.17 #24190 review and approve 0.90 #24182 review discussion and the change and approve 0.08 #24178 review and briefly comment 0.33 #24177 review, research and comment 0.08 #24187 brief follow-up 0.43 #24176 research, review and approve 0.27 #24191 research, testing 0.20 #24192 review and approve 0.38 #24056 debugging
0.58 #24056 debugging, something in find_lexical_cv()?
3.85
2026/02/16 Monday 0.52 github notifications 0.08 #24178 review updates and approve 2.20 #24098 review and comments 0.88 #24056 more debugging, find at least one bug 0.92 #24056 work up tests, testing, commit message and push for
CI, perldelta and re-push
4.60
2026/02/17 Tuesday 0.18 #24056 check CI results, rebase in case and re-push, open PR 24205 2.88 #24187 review, comments 0.47 #24187 more comments 0.23 reply email from Jim Keenan re git handling for testing PR
tests without the fixes
3.76
2026/02/18 Wednesday 3.02 #24187 review comments, work on fix for assertion, testing, push for CI 0.25 #24187 check CI, make perldelta and make PR 24211
0.35 #24098 review updates and approve
3.62
2026/02/19 Thursday 0.30 #24200 research and comment 0.47 #24215 review, wonder why cmp_version didn’t complain, find out and approve 0.08 #24208 review and comment 0.73 #24213 review, everything that needs saying had been said 0.22 #24206 review and comments 0.53 #24203 review, comment and approve 0.33 #24210 review, research and approve with comment
0.37 #24200 review, research and approve
3.03
2026/02/23 Monday 0.35 #24212 testing add #24213 to 5.42 votes 2.42 #24159 review and benchmarking, comment
0.73 #24187 try to break it
3.50
2026/02/24 Tuesday 0.35 github notifications 1.13 #24187 update PR 24211 commit message, rechecks 0.43 #24001 re-work tests on PR 24060
0.30 #24001 more re-work
2.21
2026/02/25 Wednesday 1.02 #24180 research, comments 0.22 #24206 review update and comment 0.28 #24208 review updates and comment 0.57 #24060 more tests
0.88 #24060 more tests, testing, debugging
2.97
2026/02/26 Thursday 0.47 #24211 minor fixes per comments 0.23 #24206 review updates and approve 0.22 #24180 review updates and approve 0.98 #24236 review and comments 1.30 #24228 review, testing and comments 0.08 #24236 research and comment
0.78 #24159 review updates, testing, comments
4.06
Which I calculate is 59.25 hours.
Approximately 50 tickets were reviewed or worked on, and 3 patches were applied. ```
Let’s talk about music programming! There are a million aspects to this subject, but today, we’ll touch on generating rhythmic patterns with mathematical and combinatorial techniques. These include the generation of partitions, necklaces, and Euclidean patterns.
Stefan and J. Richard Hollos wrote an excellent little book called “Creating Rhythms” that has been turned into C, Perl, and Python. It features a number of algorithms that produce or modify lists of numbers or bit-vectors (of ones and zeroes). These can be beat onsets (the ones) and rests (the zeroes) of a rhythm. We’ll check out these concepts with Perl.
For each example, we’ll save the MIDI with the MIDI::Util module. Also, in order to actually hear the rhythms, we will need a MIDI synthesizer. For these illustrations, fluidsynth will work. Of course, any MIDI capable synth will do! I often control my eurorack analog synthesizer with code (and a MIDI interface module).
Here’s how I start fluidsynth on my mac in the terminal, in a separate session. It uses a generic soundfont file (sf2) that can be downloaded here (124MB zip).
fluidsynth -a coreaudio -m coremidi -g 2.0 ~/Music/soundfont/FluidR3_GM.sf2
So, how does Perl know what output port to use? There are a few ways, but with JBARRETT’s MIDI::RtMidi::FFI::Device, you can do this:
use MIDI::RtMidi::FFI::Device ();
my $midi_in = RtMidiIn->new;
my $midi_out = RtMidiOut->new;
print "Input devices:\n";
$midi_in->print_ports;
print "\n";
print "Output devices:\n";
$midi_out->print_ports;
print "\n";
This shows that fluidsynth is alive and ready for interaction.
Okay, on with the show!
First-up, let’s look at partition algorithms. With the part() function, we can generate all partitions of n, where n is 5, and the “parts” all add up to 5. Then taking one of these (say, the third element), we convert it to a binary sequence that can be interpreted as a rhythmic phrase, and play it 4 times.
#!/usr/bin/env perl
use strict;
use warnings;
use Music::CreatingRhythms ();
my $mcr = Music::CreatingRhythms->new;
my $parts = $mcr->part(5);
# [ [ 1, 1, 1, 1, 1 ], [ 1, 1, 1, 2 ], [ 1, 2, 2 ], [ 1, 1, 3 ], [ 2, 3 ], [ 1, 4 ], [ 5 ] ]
my $p = $parts->[2]; # [ 1, 2, 2 ]
my $seq = $mcr->int2b([$p]); # [ [ 1, 1, 0, 1, 0 ] ]
Now we render and save the rhythm:
use MIDI::Util qw(setup_score);
my $score = setup_score(bpm => 120, channel => 9);
for (1 .. 4) {
for my $bit ($seq->[0]->@*) {
if ($bit) {
$score->n('en', 40);
}
else {
$score->r('en');
}
}
}
$score->write_score('perldotcom-1.mid');
In order to play the MIDI file that is produced, we can use fluidsynth like this:
fluidsynth -i ~/Music/soundfont/FluidR3_GM.sf2 perldotcom-1.mid
Not terribly exciting yet.
Let’s see what the “compositions” of a number reveal. According to the Music::CreatingRhythms docs, a composition of a number is “the set of combinatorial variations of the partitions of n with the duplicates removed.”
Okay. Well, the 7 partitions of 5 are:
[[1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 1, 3], [1, 2, 2], [1, 4], [2, 3], [5]]
And the 16 compositions of 5 are:
[[1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 1, 2, 1], [1, 1, 3], [1, 2, 1, 1], [1, 2, 2], [1, 3, 1], [1, 4], [2, 1, 1, 1], [2, 1, 2], [2, 2, 1], [2, 3], [3, 1, 1], [3, 2], [4, 1], [5]]
That is, the list of compositions has, not only the partition [1, 2, 2], but also its variations: [2, 1, 2] and [2, 2, 1]. Same with the other partitions. Selections from this list will produce possibly cool rhythms.
Here are the compositions of 5 turned into sequences, played by a snare drum, and written to the disk:
use Music::CreatingRhythms ();
use MIDI::Util qw(setup_score);
my $mcr = Music::CreatingRhythms->new;
my $comps = $mcr->compm(5, 3); # compositions of 5 with 3 elements
my $seq = $mcr->int2b($comps);
my $score = setup_score(bpm => 120, channel => 9);
for my $pattern ($seq->@*) {
for my $bit (@$pattern) {
if ($bit) {
$score->n('en', 40); # snare patch
}
else {
$score->r('en');
}
}
}
$score->write_score('perldotcom-2.mid');
A little better. Like a syncopated snare solo.
Sidebar
Another way to play the MIDI file is to use timidity. On my mac, with the soundfont specified in the timidity.cfg configuration file, this would be:
timidity -c ~/timidity.cfg -Od perldotcom-2.mid
To convert a MIDI file to an mp3 (or other audio formats), I do this:
timidity -c ~/timidity.cfg perldotcom-2.mid -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k perldotcom-2.mp3
Okay. Enough technical details! What if we want a kick bass drum and hi-hat cymbals, too? Refactor time…
use MIDI::Util qw(setup_score);
use Music::CreatingRhythms ();
my $mcr = Music::CreatingRhythms->new;
my $s_comps = $mcr->compm(4, 2); # snare
my $s_seq = $mcr->int2b($s_comps);
my $k_comps = $mcr->compm(4, 3); # kick
my $k_seq = $mcr->int2b($k_comps);
my $score = setup_score(bpm => 120, channel => 9);
for (1 .. 8) { # repeats
my $s_choice = $s_seq->[ int rand @$s_seq ];
my $k_choice = $k_seq->[ int rand @$k_seq ];
for my $i (0 .. $#$s_choice) { # pattern position
my @notes = (42); # hi-hat every time
if ($s_choice->[$i]) {
push @notes, 40;
}
if ($k_choice->[$i]) {
push @notes, 36;
}
$score->n('en', @notes);
}
}
$score->write_score('perldotcom-3.mid');
Here we play generated kick and snare patterns, along with a steady hi-hat.
Next up, let’s look at rhythmic “necklaces.” Here we find many grooves of the world.

Image from The Geometry of Musical Rhythm
Rhythm necklaces are circular diagrams of equally spaced, connected nodes. A necklace is a lexicographical ordering with no rotational duplicates. For instance, the necklaces of 3 beats are [[1, 1, 1], [1, 1, 0], [1, 0, 0], [0, 0, 0]]. Notice that there is no [1, 0, 1] or [0, 1, 1]. Also, there are no rotated versions of [1, 0, 0], either.
So, how many 16 beat rhythm necklaces are there?
my $necklaces = $mcr->neck(16);
print scalar @$necklaces, "\n"; # 4116 of 'em!
Okay. Let’s generate necklaces of 8 instead, pull a random choice, and play the pattern with a percussion instrument.
use MIDI::Util qw(setup_score);
use Music::CreatingRhythms ();
my $patch = shift || 75; # claves
my $mcr = Music::CreatingRhythms->new;
my $necklaces = $mcr->neck(8);
my $choice = $necklaces->[ int rand @$necklaces ];
my $score = setup_score(bpm => 120, channel => 9);
for (1 .. 4) { # repeats
for my $bit (@$choice) { # pattern position
if ($bit) {
$score->n('en', $patch);
}
else {
$score->r('en');
}
}
}
$score->write_score('perldotcom-4.mid');
Here we choose from all necklaces. But note that this also includes the sequence with all ones and the sequence with all zeroes. More sophisticated code might skip these.
More interesting would be playing simultaneous beats.
use MIDI::Util qw(setup_score);
use Music::CreatingRhythms ();
my $mcr = Music::CreatingRhythms->new;
my $necklaces = $mcr->neck(8);
my $x_choice = $necklaces->[ int rand @$necklaces ];
my $y_choice = $necklaces->[ int rand @$necklaces ];
my $z_choice = $necklaces->[ int rand @$necklaces ];
my $score = setup_score(bpm => 120, channel => 9);
for (1 .. 4) { # repeats
for my $i (0 .. $#$x_choice) { # pattern position
my @notes;
if ($x_choice->[$i]) {
push @notes, 75; # claves
}
if ($y_choice->[$i]) {
push @notes, 63; # hi_conga
}
if ($z_choice->[$i]) {
push @notes, 64; # low_conga
}
$score->n('en', @notes);
}
}
$score->write_score('perldotcom-5.mid');
And that sounds like:
How about Euclidean patterns? What are they, and why are they named for a geometer?
Euclidean patterns are a set number of positions P that are filled with a number of beats Q that is less than or equal to P. They are named for Euclid because they are generated by applying the “Euclidean algorithm,” which was originally designed to find the greatest common divisor (GCD) of two numbers, to distribute musical beats as evenly as possible.
use MIDI::Util qw(setup_score);
use Music::CreatingRhythms ();
my $mcr = Music::CreatingRhythms->new;
my $beats = 16;
my $s_seq = $mcr->rotate_n(4, $mcr->euclid(2, $beats)); # snare
my $k_seq = $mcr->euclid(2, $beats); # kick
my $h_seq = $mcr->euclid(11, $beats); # hi-hats
my $score = setup_score(bpm => 120, channel => 9);
for (1 .. 4) { # repeats
for my $i (0 .. $beats - 1) { # pattern position
my @notes;
if ($s_seq->[$i]) {
push @notes, 40; # snare
}
if ($k_seq->[$i]) {
push @notes, 36; # kick
}
if ($h_seq->[$i]) {
push @notes, 42; # hi-hats
}
if (@notes) {
$score->n('en', @notes);
}
else {
$score->r('en');
}
}
}
$score->write_score('perldotcom-6.mid');
Now we’re talkin’ - an actual drum groove! To reiterate, the euclid() method distributes a number of beats, like 2 or 11, over the number of beats, 16. The kick and snare use the same arguments, but the snare pattern is rotated by 4 beats, so that they alternate.
So what have we learned today?
-
That you can use mathematical functions to generate sequences to represent rhythmic patterns.
-
That you can play an entire sequence or simultaneous notes with MIDI.
References:
For those running a development version of git from master or next, you probably have seen it already. Today I was inspecting the git logs of git and found this little gem. It supports my workflow to the max.
You can now configure git status to compare branches with your current branch
in status. When you configure status.comparebranches you can use
@{upstream} and @{push} and you see both how far you’ve diverged from your
upstream and your push branch. For those, like me, who track an upstream branch
which differs from their push branch this is a mighty fine feature!
TL;DR
I didn’t like how the default zsh prompt truncation works. My solution, used in
my own custom-made prompt (fully supported by promptinit), uses a custom
precmd hook to dynamically determine the terminal’s available width.
Instead of blind chopping, my custom logic ensures human-readable truncation by following simple rules: it always preserves the home directory (∼) and the current directory name, only removing or shortening non-critical segments in the middle to keep the PS1 clean, contextual, and perfectly single-line. This is done via a so-called “Zig-Zag” pattern or string splitting on certain delimiters.
















