An interesting curiosity, as you say. Because 10 is an exact multiple of 2, all binary fractions have an exact, non-repeating decimal representation (but not vice versa). That means we're guaranteed to terminate.
The code compiles cleanly with an aggressive set of warnings enabled, and Valgrind is completely happy with the test program. But I expect you already know that.
There's very little I would change, but I did notice a redundancy here:
if (expo < 0) { while (expo < 0) { expo++; ad_div(&a); } }
The outer if is pointless (and a good compiler will ignore it), so it's just useless clutter.
On the style side, I'd move the mutation of i from the body of these for loops into the loop-control part (inside the ( )), so that the control variable is constant within the body of each iteration:
for (size_t i = msd; i > lsd;) { i--; //... }
for (size_t i = msd; i > lsd;) { printf("%d", a->digit[--i]); //... }
to become, respectively:
for (size_t i = msd; i-- > lsd;) {
//...
}
for (size_t i = msd; i-- > lsd;) {
printf("%d", a->digit[i]);
//...
}
That makes no difference to the functionality, but is less surprising and makes it easier to reason about the code. (If you want to be cute, you can write those operators together without a space, aka the infamous "goes to" operatorinfamous "goes to" operator, -->.)
One line that might need more comments is this one:
// Max fractional decimal digits in a `double`. #define AD_F (DBL_MANT_DIG - DBL_MIN_EXP)
As we all know, DBL_MANT_DIG and DBL_MIN_EXP are defined in terms of FLT_RADIX, so it looks like an error to use these to infer the number of decimal digits required. A small amount of mathematical reasoning shows that each added bit requires one more decimal digit for the representation; I recommend that you summarise that in the comment to show that it's not a mistake.