Expand description
A call to the library, folded into the call it really is.
Section 20.2 of spec/optimizer/20-idioms-and-libcalls.md, the half of it that is about a
library call rather than about arithmetic. printf("hello world\n") writes the same bytes as
puts("hello world"), and the second one does not read a format string at run time, so gcc
rewrites it and has done since 2000. A program that checks which of the two it was left with,
which is what gcc.c-torture/execute/builtins/printf.c does by defining a printf of its own
that aborts, fails outright on a compiler that leaves the call alone. tamnd/rucc#1636 is that
program and the two beside it.
§The rules
All of them measured against gcc 16.2.0 on x86-64 rather than read out of its source, and all of them conditional on the format being a string this module holds the bytes of.
strstr(s, "") is s, since the empty string is found at once wherever it is looked for.
strstr(s, "w") is strchr(s, 'w'), which is a search for a character rather than for a string
and is worth doing wherever the haystack came from. strstr of two strings this module holds is
the answer itself, which is a place in the haystack or a null pointer, and nothing is called.
strlen, strnlen, strcmp, strncmp, strchr, strrchr, memchr, strspn, strcspn and
strpbrk of strings this module holds are the answer itself in the same way, which is a number
for the first four and the last two of the six that search, and a place in the first argument or
a null pointer for the other ones. The number goes in with the width the call was declared to
give back, because a program that declared strlen as something returning an int is a program
whose reader of that answer reads an int.
Three of them have a rule about the shape rather than about the bytes. strpbrk(s, "") is a
null pointer and strspn(s, "") is zero, since nothing at all is in an empty set, and
strcspn(s, "") is strlen(s) for the same reason read the other way. strpbrk(s, "c") is
strchr(s, 'c'), which is the strstr rule again for a set of one character instead of a
needle of one.
Two of them are told how far to read rather than going looking for a terminator, and those two
read the object’s bytes rather than the string in it. memchr(s, c, n) needs the object to have
n bytes from s on, and it is refused where it does not, since a call reading past the end of
what the compiler can see is a call whose answer the compiler does not know. strnlen(s, n) is
the count where nothing terminated the string inside it.
printf with the format alone: nothing at all when it is empty, putchar when it is one
character, and puts of the format without its last character when the format holds no % and
ends in a newline. printf("%s\n", p) is puts(p) and printf("%c", c) is putchar(c),
whatever p and c are. printf("%s", p) where p is a string this module holds is the same
question again asked of that string, and where it is not, the call stays: printf has no stream
argument to hand to fputs, and stdout is not a name a compiler may invent.
fprintf is the same list with a stream in hand, so the case printf cannot take is the case
this one can. A format holding no % becomes fputc of its one character or fwrite of the
whole of it, fprintf(s, "%c", c) becomes fputc(c, s), and fprintf(s, "%s", p) becomes
fputs(p, s) however little is known about p.
fputs(p, s) needs the length of p and nothing else. Zero is nothing at all, one is fputc
when the character is known as well, and anything longer is fwrite(p, 1, len, s).
The _unlocked spellings get the one fold that names no function, which is that a call writing
nothing is removed. gcc stops in exactly the same place and the reason is in the torture
program’s own comment: a system need not have a puts_unlocked for the compiler to name.
The checking spellings _FORTIFY_SOURCE writes, __memcpy_chk and the thirteen beside it,
carry the size of the destination as one more argument and abort when the call would not fit.
Where that size is all ones, which is __builtin_object_size not knowing, or where what the
call writes is known to fit, the check cannot fail and the call is the plain one without it.
Where it can fail the call may still get cheaper: a checking stpcpy whose answer nothing reads
is a checking strcpy, and a checking strcpy of a known string is a checking memcpy. An
append of nothing is the destination. The plain name has to be one the module does not declare
with some other shape, and a call made plain is looked at again, up to three times.
§What a call has to be
For the printf family, its result has to be read by nothing. printf answers the number of
characters written and puts answers a non-negative number that is not that count, so a program
looking at the answer is a program this may not touch. The str and mem families are the other
way round: the answer is the whole point of the call and the fold produces it, so a program
reading it is the ordinary case.
It has to give back one value of the kind its name says it does. A program that declared
strchr as something returning two values, or a number, declared a function of its own and a
pointer into a string literal is not what it answers.
The name has to be the one the source spelled rather than the one the object file will carry.
extern char *strstr (const char *, const char *) __asm ("my_strstr"); is a declaration of
strstr, and a compiler that reads the symbol alone sees a call to a function it knows nothing
about. So the callee is looked up through rucc_ir::Func::spelled, and a call this leaves
behind is a call to whatever symbol the module says that name has, which is the rename again
read from the other end.
The name has to be one this module does not define. A translation unit holding the body of its
own fputs means that body, which is the rule crate::heap applies to malloc and for the
same reason.
The function must not carry memory SSA yet, which where this runs it does not. Memory is
threaded by crate::number, that pass is in the function pipeline, and this runs before the
pipeline starts. The check is here anyway, because a call with a memory operand rewritten into
one without would be a use of a value nothing defines.
§Where it runs, and why it is not a rule
Section 20.2 asks for folds like these to be rules in the rewrite DSL with the callee’s identity
in the pattern, and most of them can be. These cannot. A rule rewrites one instruction into
instructions, and two of the rewrites here need something no rule has: the name puts has to be
interned before a call can name it, and printf("hello world\n") has to leave behind a string
that is not in the module yet, because “hello world” with a terminator is not a suffix of
“hello world\n” with one. So this is a module at a time transformation beside crate::ipcp
and crate::ipasra, which is where the interner and the module both are.
-O1 and above, which is one level below where those two run. gcc folds these at -O1, the
torture programs are compiled at every level from -O1 up, and the fold makes the program
smaller as well as faster, so there is no level above -O0 where declining it is right.
Off under -fno-builtin and -ffreestanding, which is the flag pair section 20.1 describes,
and off for one name at a time under -fno-builtin-<name>. A freestanding program left with a
call to a puts it never wrote is a link failure, and that is the whole reason the flag exists.
Constants§
- NAME
- What the pass is called in
-fopt-infoand-fpass-fuel=.
Functions§
- fold
- Folds every call in the module whose output the compiler can work out.