Proven C Book한국어 GitHub

91 Formatting and parsing — not writing the type twice

What to know first

chapter 58, Variadic functions · variadic arguments and the format string
chapter 61, The terrain of the standard library · the printf contract

Looking back

Chapter 58 said type information does not ride along into variadic arguments, and chapter 61 said that is why the format string alone settles “how the stack is to be read”. Then what is needed to take the type from the argument?

A. Catch the type at the call site and send it along with the value. That is, do not pass the argument as it is but wrap it into a {type tag, value} bundle. The remaining problem is “how is the type tag attached automatically”, and the answer is C11′s _Generic — the device that chooses different code at compile time according to an expression’s type. The run-time cost is zero, and unlike the implicit conversion rules learned in chapter 29, here the type is preserved.

The need for this chapter, and its context

Chapter 58 showed why variadic functions are not type-safe, and chapter 85 raised it as the third bug. This is the answer, and it rightly comes after strings (89) — because what formatting produces is a string. You need the vessel before you can talk about printing into it.

By the end of this chapter

The answer to chapter 85′s third bug — the mismatch of format and argument. How the typeless placeholder {} obtains type safety, what _Generic does beneath it, and how failure appears as a value in the opposite direction, parsing. It is the alternative to the printf and scanf taken apart in chapter 61.

The questions this chapter answers

  1. What exactly does PROVEN_ARG do?
  2. Does the scanner have a format string too?

91.1 {} — the placeholder with no type

The rules are only three.

There being no %d, there is no place for a %d and a double to go out of step. The possibility of mismatch seen in chapter 61 is removed at the level of syntax.

91.1.1 The whole grammar after the colon

The order of the specifiers is fixed, and every one may be omitted.

placewhat may be writtenmeaning
fillany characterthe character filling the spare places. it comes before the alignment symbol
align< > ^left, right, centre. the default is right for numbers, left otherwise
sign+attach a sign to positives too
alternative form#attach the 0x, 0b, 0 prefix
zero fill0put before the width to fill with zeros (it goes after the sign)
widtha numberthe minimum number of characters. it does not cut if over
precision.numberdecimal places for reals
typex X o b f e ghexadecimal (lower, upper), octal, binary, fixed, exponential, shortest

Table 92.1

They correspond to chapter 61′s printf formats but differ in three ways. The alignment symbol comes first ({:<10} instead of %-10s), the fill character can be chosen ({:*>8}), and there is no type letter (%d’s d has gone — the type comes from the argument).

examples-en/ch91/spec.c

/* The whole placeholder syntax — fill, alignment, width, precision, form, and
   user types. It starts at a single {} and goes as far as drawing a table. */
#include <proven.h>

/* how to print a type the library knows nothing of: hand it one function that draws it */
typedef struct { int num; int den; } frac_t;

static proven_err_t render_frac(proven_fmt_sink_t out, const void *obj)
{
    const frac_t *f = (const frac_t *)obj;
    proven_byte_t buf[32];
    proven_u8str_t s = proven_u8str_borrow(buf, sizeof buf);
    proven_fmt_result_t r = proven_u8str_append_fmt(&s, "{}/{}",
                                                    PROVEN_ARG(f->num),
                                                    PROVEN_ARG(f->den));
    if (!proven_is_ok(r.err)) return r.err;
    return proven_fmt_put(out, proven_u8str_as_view(&s));
}

int main(void)
{
    proven_byte_t buf[256];

    /* ── (1) alignment and fill ──────────────────────────────────── */
    proven_println("|{:>8}|{:<8}|{:^8}|  (right, left, centre)",
                   PROVEN_ARG("ab"), PROVEN_ARG("ab"), PROVEN_ARG("ab"));
    proven_println("|{:*>8}|{:-<8}|{:.^8}|  (the fill character goes in front)",
                   PROVEN_ARG("ab"), PROVEN_ARG("ab"), PROVEN_ARG("ab"));
    proven_println("|{:08}|{:+}|{:+}|      (zero fill, forced sign)",
                   PROVEN_ARG(42), PROVEN_ARG(42), PROVEN_ARG(-42));

    /* ── (2) bases and the alternative forms ─────────────────────── */
    proven_println("{:x} {:X} {:#x} {:o} {:b} {:#b}",
                   PROVEN_ARG(255), PROVEN_ARG(255), PROVEN_ARG(255),
                   PROVEN_ARG(8), PROVEN_ARG(5), PROVEN_ARG(5));

    /* ── (3) reals — digits and form ─────────────────────────────── */
    proven_println("{} {:.2} {:.0} {:f} {:e} {:g}",
                   PROVEN_ARG(3.14159), PROVEN_ARG(3.14159), PROVEN_ARG(3.14159),
                   PROVEN_ARG(3.14159), PROVEN_ARG(3.14159), PROVEN_ARG(3.14159));
    proven_println("very large / very small: {} {}",
                   PROVEN_ARG(1e20), PROVEN_ARG(5e-7));

    /* ── (4) the braces themselves ───────────────────────────────── */
    proven_println("a brace is written {{ or }}");

    /* ── (5) a user type ─────────────────────────────────────────── */
    frac_t half = { .num = 1, .den = 2 };
    proven_arg_t a = proven_arg_custom(&half, render_frac);
    proven_println("a user type: {} (it takes a width too: |{:>8}|)",
                   PROVEN_ARG(a), PROVEN_ARG(a));

    /* ── (6) the three roads of formatting — refuse / truncate / grow ─ */
    proven_u8str_t small = proven_u8str_borrow(buf, 8);   /* up to 7 bytes of content */

    proven_fmt_result_t r1 = proven_u8str_append_fmt(&small, "{}", PROVEN_ARG("far too long to fit"));
    proven_println("append_fmt        err={} written={} required={}",
                   PROVEN_ARG((int)r1.err), PROVEN_ARG(r1.written), PROVEN_ARG(r1.required));

    proven_fmt_result_t r2 = proven_u8str_append_fmt_trunc(&small, "{}", PROVEN_ARG("abcdefghij"));
    proven_println("append_fmt_trunc  err={} written={} required={} content=\"{}\"",
                   PROVEN_ARG((int)r2.err), PROVEN_ARG(r2.written), PROVEN_ARG(r2.required),
                   PROVEN_ARG(proven_u8str_as_view(&small)));

    proven_allocator_t alloc = proven_heap_allocator();
    proven_result_u8str_t made = proven_u8str_create(alloc, 4);
    if (proven_is_ok(made.err)) {
        proven_u8str_t g = made.value;
        proven_fmt_result_t r3 = proven_u8str_append_fmt_grow(alloc, &g, "{} {} {}",
                                                              PROVEN_ARG("it grows"),
                                                              PROVEN_ARG(2026),
                                                              PROVEN_ARG(true));
        proven_println("append_fmt_grow   err={} content=\"{}\"",
                       PROVEN_ARG((int)r3.err), PROVEN_ARG(proven_u8str_as_view(&g)));
        proven_u8str_destroy(alloc, &g);
    }

    /* ── (7) drawing a table — what a width is really for ────────── */
    proven_println("");
    proven_println("{:<10}{:>6}{:>9}", PROVEN_ARG("name"), PROVEN_ARG("count"), PROVEN_ARG("share"));
    proven_println("{:-<25}", PROVEN_ARG(""));
    const char *names[] = { "alpha", "beta", "gamma" };
    int         counts[] = { 7, 128, 3 };
    for (int i = 0; i < 3; i++)
        proven_println("{:<10}{:>6}{:>9.2}", PROVEN_ARG(names[i]),
                       PROVEN_ARG(counts[i]), PROVEN_ARG(counts[i] / 138.0 * 100));
    return 0;
}

Output

|      ab|ab      |   ab   |  (right, left, centre)
|******ab|ab------|...ab...|  (the fill character goes in front)
|00000042|+42|-42|      (zero fill, forced sign)
ff FF 0xff 10 101 0b101
3.141590 3.14 3 3.141590 3.141590e+00 3.14159
very large / very small: 1.000000e+20 5.000000e-07
a brace is written { or }
a user type: 1/2 (it takes a width too: |     1/2|)
append_fmt        err=2 written=0 required=19
append_fmt_trunc  err=2 written=7 required=10 content="abcdefg"
append_fmt_grow   err=0 content="it grows 2026 true"

name       count    share
-------------------------
alpha          7     5.07
beta         128    92.75
gamma          3     2.17

The example shows this table in the flesh, a line at a time. A few points.

① The fill is written before the alignment. {:*>8} is “fill with asterisks, align right”. Swap the order ({:>*8}) and it does not mean anything.

② The 0 of {:08} goes after the sign. Zero-fill -42 to a width of 8 and it is -0000042, not 000000-42 — the same rule as %08d seen in chapter 61.

③ The default notation for reals differs from printf’s. Very large and very small numbers are printed by printf("%f") as 100000000000000000000.000000 or 0.000000, while this library uses exponential notation, 1.000000e+20 and 5.000000e-07. The side that does not lose information was taken as the default, and it is a difference to know before comparing two logs. If fixed notation is needed, force it with {:f}.

{:g} gives the shortest notation that round-trips. That 3.14159 comes out as it stands is that result — the notation keeping the “read it back and it is the same value” property seen in chapter 8.

91.1.2 Printing a type the library has never heard of

What can go into {} is only the types in the _Generic list. Then how is my own struct printed — give it one function that draws.

static proven_err_t render_frac(proven_fmt_sink_t out, const void *obj)
{
    const frac_t *f = obj;
    /* ... make it ... */
    return proven_fmt_put(out, view);      /* and send it out */
}

proven_arg_t a = proven_arg_custom(&half, render_frac);
proven_println("{} and |{:>8}|", PROVEN_ARG(a), PROVEN_ARG(a));

proven_fmt_sink_t is “a hole that receives bytes”, and proven_fmt_put sends them out. As the example’s output shows, width and alignment apply to a user type too.

There is one contract to know here. The drawing function is called twice per {} — once with a counting sink (because the width and alignment must be calculated) and once for real. So this function must be deterministic and must not mutate its target. If the two results disagree the library returns INVALID_ARG rather than print a misaligned field. It is the price paid for aligning without allocating.

examples-en/ch91/fmt.c

#include <proven.h>
#include <stdio.h>

int main(void)
{
    /* formatted into a string rather than onto the screen — the buffer is borrowed from the stack */
    proven_byte_t buf[64];
    proven_u8str_t out = proven_u8str_borrow(buf, sizeof buf);

    int      port = 8080;
    double   load = 0.4237;
    const char *host = "example.org";

    proven_fmt_result_t r = proven_u8str_append_fmt(&out, "{}:{} load={:.2}",
                                                   PROVEN_ARG(host), PROVEN_ARG(port),
                                                   PROVEN_ARG(load));
    if (proven_is_ok(r.err)) {
        proven_u8str_view_t v = proven_u8str_as_view(&out);
        printf("formatted : %.*s\n", (int)v.size, (const char *)v.ptr);
    }

    /* and if the vessel is too small? it refuses rather than truncating */
    proven_byte_t small_buf[8];
    proven_u8str_t small = proven_u8str_borrow(small_buf, sizeof small_buf);
    proven_fmt_result_t r2 = proven_u8str_append_fmt(&small, "{}:{}",
                                                    PROVEN_ARG(host), PROVEN_ARG(port));
    printf("into 8 bytes: %s (err=%d)\n",
           proven_is_ok(r2.err) ? "ok" : "refused", (int)r2.err);

    /* alignment and digits — the width and precision of chapter 52 */
    proven_u8str_t line = proven_u8str_borrow(buf, sizeof buf);
    (void)proven_u8str_reset(&line);
    proven_fmt_result_t r3 = proven_u8str_append_fmt(&line, "|{:>10}|{:<10}|{:.3}|",
                                                    PROVEN_ARG(host), PROVEN_ARG(host),
                                                    PROVEN_ARG(load));
    if (proven_is_ok(r3.err)) {
        proven_u8str_view_t v = proven_u8str_as_view(&line);
        printf("aligned   : %.*s\n", (int)v.size, (const char *)v.ptr);
    }
    return 0;
}

Output

formatted : example.org:8080 load=0.42
into 8 bytes: refused (err=2)
aligned   : |example.org|example.org|0.424|

This example formats not to the screen but into a string — the proven_println seen in chapter 86 is the edition connecting this machinery to standard output. Three things can be pointed out.

First, formatting too can fail. example.org:8080 cannot go into an 8-byte vessel, so it was refused. Chapter 90′s principle stands here too — rather than truncate, it returns a failure. The opposite default from snprintf.

Second, the format specification syntax is a little different. The > of {:>10} is right alignment, {:<10} left alignment, {:.3} three decimal places. They correspond to chapter 61′s %10s, %-10s and %.3f, differing in that there is no type letter.

Third, it rounds but keeps the number of digits. load=0.42 is what {:.2} made.

91.2 Which formatting function to use

Chapter 90′s three kinds are here in formatting too. Organised in a table there is nothing to choose over.

functionwhen shortallocatorwhere it is used
proven_println(fmt, …)not neededone line to the screen
proven_print(fmt, …)not neededwithout a line break
proven_eprint(fmt, …)not neededto standard error
proven_u8str_append_fmtrefuses (the original stands)not neededa fixed buffer. the default
proven_u8str_append_fmt_truncas much as fitsnot neededplaces that may be cut, such as a log line
proven_u8str_append_fmt_growgrowsneededwhen the length is unknown
proven_u8str_append_fmt_with_scratchgrowsneeded (+ scratch)when the temporary memory is to be given separately

Table 92.2

All of them return a proven_fmt_result_t, and this bundle has two more numbers beside err.

typedef struct {
    proven_err_t  err;
    proven_size_t written;    /* the number of bytes actually written */
    proven_size_t required;   /* the number of bytes needed to write it all */
} proven_fmt_result_t;

required is the same information as snprintf’s return value seen in chapter 61. The difference is that it sits in a named slot — with snprintf a human had to remember the convention “if the return value is at least the buffer size it was truncated”, while here err already says that and required answers “so how much was needed”. In the output of the example spec.c, written=7 required=10 is that use — how much to enlarge the buffer by is known as it stands.

Q. What exactly does PROVEN_ARG do?

A. It chooses on the argument’s type with _Generic and makes a small struct with a tag fitting that type attached. Carried over in concept alone it has this shape.

#define PROVEN_ARG(x) _Generic((x),          \
    int:          proven_arg_i32,            \
    double:       proven_arg_f64,            \
    const char *: proven_arg_cstr,           \
    bool:         proven_arg_bool            \
    /* ... */ )(x)

_Generic chooses the branch at compile time, so there is no run-time cost of determining the type. And passing a type not in the list is a compile error — the exact opposite of chapter 61′s printf, which accepted anything.

Counter-example. Passing a value without PROVEN_ARG

proven_println("count={}", count);        /* it does not compile */

A raw value rather than a bundle was passed, so the types do not match and the build fails. It can feel tiresome, but this is the point of the design — forget to wrap it and the program is not made. Herein lies the difference from printf, which compiles even when you forget and goes strange during execution.

A common misconception. “The placeholder has no type, so it must be slow”

The opposite. printf interprets the format string letter by letter during execution and decides what to take out next. {} scans the format too, but each argument’s type is already settled by its tag, so there is no guessing. Above all, there being no UB from type mismatch, no defensive code is needed either. The difference in cost is mostly negligible, and this library’s real-number formatting has rather taken more care over accuracy — reproducing exactly the rounding rules of %f seen in chapter 61 is the more awkward task.

91.3 The opposite direction — the scanner

Parsing is formatting’s mirror, but the character of failure differs. Formatting fails only when the vessel is too small, while parsing fails whenever the input differs from expectation. And as seen in chapter 61, sscanf tells only “how many succeeded” — it does not say where or why it stopped.

proven’s scanner is an object with a cursor. It is placed over a view and reads onward one at a time. Each read gives its result as a bundle.

typedef struct {
    proven_u8str_view_t view;     /* the input being read (borrowed) */
    proven_size_t       cursor;   /* how far it has read */
} proven_scan_t;

That there are only two slots says two things. First, the scanner does not own the input — it is only a cursor laid over a view, so making it allocates nothing and there is no destroying. Second, the cursor can be saved and restored by hand. Copy the whole struct, and on failure put it back, so a parser that “looks a few characters ahead to judge” is not hard to write.

proven_scan_t save = sc;         /* a mark to go back to */
proven_result_i64_t n = proven_scan_i64(&sc);
if (!proven_is_ok(n.err)) sc = save;   /* it failed, so let it never have happened */

There are six reading functions, and all of them push the cursor forward.

functionwhat it readswhat it returns
proven_scan_i64(&sc)a signed integer{err, val}
proven_scan_u64(&sc)an unsigned integer{err, val}
proven_scan_f64(&sc)a real{err, val}
proven_scan_str(&sc)a word up to whitespace{err, view}a view into the original
proven_scan_skip_whitespace(&sc)skips whitespace
proven_scan_skip_until(&sc, t)skips until t appearserr (the cursor stands if absent)

Table 92.3

That proven_scan_str returns a view without copying matters — chapter 90′s “text handling without copying” holds in parsing too. In exchange that view is valid only while the original input is alive (chapter 88).

examples-en/ch91/lines.c

#include <proven.h>
#include <stdio.h>

/* A line of the shape "name age" interpreted — failure surfaces as a value. */
static void parse_line(const char *line)
{
    proven_scan_t sc = proven_scan_init(proven_u8str_view_from_cstr(line));

    proven_result_u8str_view_t name = proven_scan_str(&sc);
    if (!proven_is_ok(name.err)) {
        printf("[%s] -> no name found\n", line);
        return;
    }

    proven_result_i64_t age = proven_scan_i64(&sc);
    if (!proven_is_ok(age.err)) {
        printf("[%s] -> age is not a number\n", line);
        return;
    }

    printf("[%s] -> name %.*s, age %lld\n", line,
           (int)name.val.size, (const char *)name.val.ptr, (long long)age.val);
}

int main(void)
{
    parse_line("alice 33");
    parse_line("bob thirty");
    parse_line("   ");
    return 0;
}

Output

[alice 33] -> name alice, age 33
[bob thirty] -> age is not a number
[   ] -> no name found

That three inputs divided into three branches is the heart of it. bob thirty had its name read but stopped at the age, and the line of whitespace only failed from the name. With sscanf both would have been lumped together as “one item succeeded” or “0”.

Having a cursor has another advantage. How far it has read can be known, so the remaining part can be handled another way or the position can be carried in an error message. It is the answer to the problem chapter 25 named when parsing a line with sscanf: “you cannot know how many characters were consumed.”

Q. Does the scanner have a format string too?

A. It does. It is written like this.

proven_scan_fmt_cursor(&sc, "{}:{}",
                       PROVEN_SCAN_ARG(&host), PROVEN_SCAN_ARG(&port));

It is symmetrical with formatting, and the arguments are wrapped addresses of the places to hold the results. But there is one caution the header states honestly — if it fails in the middle of the format, the values filled in up to that point have already been changed. The failure atomicity learned in chapter 87 is not guaranteed here, so if it is really needed the cursor and the destinations must be saved beforehand and restored. Writing the contract in the documentation rather than hiding it is this library’s way.

In practice. What happens when a parser is lenient

There is a problem that has come to light repeatedly in the handling of HTTP requests. If a server and a proxy interpret the same request slightly differently, an attacker can slip a second request in through that gap (request smuggling). The cause was differences such as one side generously letting odd whitespace in a header pass while the other refused strictly. The lesson is exactly the same as chapter 90′s story of encodings — do not read ambiguous input as something mended; refuse it. A parser that returns failure as a value is also a parser equipped with the means to express that refusal.

Recap

Formatting and parsing in summary.

what it doesAPInote
one line to the screenproven_println(fmt, ARG…)returns an error but does not compel
format into a stringproven_u8str_append_fmt(&s, …)refuses if short
permitting truncation…_append_fmt_truncstates the intent in the name
growing as it goes…_append_fmt_grow(alloc, …)needs an allocator
wrapping an argumentPROVEN_ARG(x)_Generic — a type not listed is a compile error
starting a scannerproven_scan_init(view)an object with a cursor
reading one at a timeproven_scan_i64/f64/strresult and failure as a bundle
reading by formatproven_scan_fmt_cursor(…)beware partial changes on mid-way failure

Table 92.4

The vocabulary for holding, making and reading back strings is equipped. Next are the tools that hold many — growing arrays, lists, ring buffers, hash maps, and the algorithms with “a guarantee even in the worst case” foretold in chapter 85.