475 lines
18 KiB
C
475 lines
18 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file generate_kquant_reference.c
|
|
* @brief ONE-TIME offline generator for ft_gguf_kquant_reference_vectors.h,
|
|
* and the cross-check that holds foundry's k-quant readers to ggml's.
|
|
*
|
|
* Foundry has no external dependencies, so it cannot link ggml at test time.
|
|
* That leaves duplicated arithmetic: upstream ggml and foundry's
|
|
* ft_gguf_dequant.c (and, when PANTHEON is defined, Pantheon's
|
|
* src/formats/gguf_dequant.c as a third copy). This program is what stops that
|
|
* duplication being a matter of hope:
|
|
*
|
|
* 1. It decodes every super-block it makes with ggml and with foundry (and
|
|
* Pantheon) and requires them to agree BIT-EXACTLY. ggml is linked as its
|
|
* own compiled shared library, so nothing here is a transcription of
|
|
* ggml — it IS ggml.
|
|
* 2. It sweeps SWEEP_BLOCKS further synthetic super-blocks per type the same
|
|
* way, so a sub-block index error that shows on some blocks and not others
|
|
* is still caught.
|
|
* 3. It freezes a subset — raw block bytes plus the fp32 words GGML produced
|
|
* from them — into a header the in-tree test suite checks against with no
|
|
* external dependency at all.
|
|
*
|
|
* Every block is SYNTHETIC and deterministic: one xorshift32 stream, seed
|
|
* KQ_SEED, drives everything, in a fixed order. Per type, in this order:
|
|
*
|
|
* - 2 blocks from ggml's own quantize_row_qN_K_ref over seeded weights
|
|
* (block 0 a smooth approximately-normal row, block 1 the same with spikes
|
|
* and a wide range), so the bytes are a real quantizer's output;
|
|
* - 2 blocks of pseudorandom bytes, which reach code points a quantizer
|
|
* never emits (every scale nibble, saturated quants);
|
|
* - 4 edge cases: all-zero; every byte 0xff with d (and dmin) at the largest
|
|
* finite fp16; pseudorandom codes with negative d (and dmin); pseudorandom
|
|
* codes with subnormal fp16 d (and dmin).
|
|
*
|
|
* The first four of each type are the ones the test runs as one contiguous
|
|
* run (the stride check), so they are the four that genuinely differ.
|
|
* Inf and NaN fp16 scales are not frozen: a NaN pins nothing (NaN != NaN).
|
|
*
|
|
* Build and run with generate_kquant_reference.sh.
|
|
*
|
|
* @status None
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_gguf_dequant.h"
|
|
#include "ft_gguf_map.h"
|
|
|
|
#ifdef PANTHEON
|
|
#include "gguf_dequant.h"
|
|
#endif
|
|
|
|
/* Upstream ggml, resolved from libggml-base at link time. The block pointer is
|
|
* declared void * so this file needs none of ggml's headers. */
|
|
void dequantize_row_q2_K(const void *x, float *y, int64_t k);
|
|
void dequantize_row_q3_K(const void *x, float *y, int64_t k);
|
|
void dequantize_row_q4_K(const void *x, float *y, int64_t k);
|
|
void dequantize_row_q5_K(const void *x, float *y, int64_t k);
|
|
void dequantize_row_q6_K(const void *x, float *y, int64_t k);
|
|
void quantize_row_q2_K_ref(const float *x, void *y, int64_t k);
|
|
void quantize_row_q3_K_ref(const float *x, void *y, int64_t k);
|
|
void quantize_row_q4_K_ref(const float *x, void *y, int64_t k);
|
|
void quantize_row_q5_K_ref(const float *x, void *y, int64_t k);
|
|
void quantize_row_q6_K_ref(const float *x, void *y, int64_t k);
|
|
|
|
#define SUPERBLOCK FT_GGML_K_SUPERBLOCK
|
|
#define KQUANT_KINDS 5
|
|
#define VECTORS_PER_KIND 8
|
|
#define SWEEP_BLOCKS 4096 /* extra synthetic super-blocks per type, compared and not frozen */
|
|
#define KQ_SEED 0x5eed1234u
|
|
#define NO_OFFSET (-1)
|
|
|
|
typedef struct {
|
|
uint32_t ggml_type;
|
|
const char *name;
|
|
size_t block_bytes;
|
|
int d_offset; /* byte offset of the fp16 super-block scale */
|
|
int dmin_offset; /* byte offset of the fp16 super-block min, or NO_OFFSET */
|
|
void (*ggml_decode)(const void *, float *, int64_t);
|
|
void (*ggml_quantize)(const float *, void *, int64_t);
|
|
} kquant_kind;
|
|
|
|
static const kquant_kind KINDS[KQUANT_KINDS] = {
|
|
{FT_GGML_TYPE_Q2_K, "Q2_K", FT_GGML_Q2_K_BLOCK_BYTES, 80, 82, dequantize_row_q2_K, quantize_row_q2_K_ref},
|
|
{FT_GGML_TYPE_Q3_K, "Q3_K", FT_GGML_Q3_K_BLOCK_BYTES, 108, NO_OFFSET, dequantize_row_q3_K, quantize_row_q3_K_ref},
|
|
{FT_GGML_TYPE_Q4_K, "Q4_K", FT_GGML_Q4_K_BLOCK_BYTES, 0, 2, dequantize_row_q4_K, quantize_row_q4_K_ref},
|
|
{FT_GGML_TYPE_Q5_K, "Q5_K", FT_GGML_Q5_K_BLOCK_BYTES, 0, 2, dequantize_row_q5_K, quantize_row_q5_K_ref},
|
|
{FT_GGML_TYPE_Q6_K, "Q6_K", FT_GGML_Q6_K_BLOCK_BYTES, 208, NO_OFFSET, dequantize_row_q6_K, quantize_row_q6_K_ref}
|
|
};
|
|
|
|
/* One frozen vector: where it came from, its bytes, and ggml's answer. */
|
|
typedef struct {
|
|
uint32_t ggml_type;
|
|
char origin[192];
|
|
unsigned char bytes[FT_GGML_Q6_K_BLOCK_BYTES];
|
|
size_t byte_count;
|
|
uint32_t expected[SUPERBLOCK];
|
|
} reference_vector;
|
|
|
|
static reference_vector vectors[KQUANT_KINDS * VECTORS_PER_KIND];
|
|
static size_t vector_count;
|
|
|
|
static int failures;
|
|
|
|
/* Deterministic xorshift32: the single source of every synthetic byte. */
|
|
static uint32_t rng_state = KQ_SEED;
|
|
|
|
static uint32_t rng_next(void)
|
|
{
|
|
rng_state ^= rng_state << 13;
|
|
rng_state ^= rng_state >> 17;
|
|
rng_state ^= rng_state << 5;
|
|
return rng_state;
|
|
}
|
|
|
|
/** Uniform in [0, 1), exact in float (24 random bits). */
|
|
static float rng_unit(void)
|
|
{
|
|
return (float)(rng_next() >> 8) * (1.0f / 16777216.0f);
|
|
}
|
|
|
|
/** Approximately normal, mean 0, unit variance: Irwin-Hall of 4 uniforms.
|
|
* Pure arithmetic, so the weights do not depend on the host's libm. */
|
|
static float rng_normal(void)
|
|
{
|
|
return (rng_unit() + rng_unit() + rng_unit() + rng_unit() - 2.0f) * 1.7320508f;
|
|
}
|
|
|
|
static uint32_t bits_of(float value)
|
|
{
|
|
uint32_t bits;
|
|
|
|
memcpy(&bits, &value, sizeof(bits));
|
|
return bits;
|
|
}
|
|
|
|
static void put_half(unsigned char *block, int offset, uint16_t half)
|
|
{
|
|
if (offset == NO_OFFSET)
|
|
return;
|
|
block[offset] = (unsigned char)(half & 0xffu);
|
|
block[offset + 1] = (unsigned char)(half >> 8);
|
|
}
|
|
|
|
/** Pantheon's reader for a type it has; *has_reader = 0 for one it does not. */
|
|
static void pantheon_decode(uint32_t ggml_type, const unsigned char *block, float *out, int *has_reader)
|
|
{
|
|
#ifdef PANTHEON
|
|
*has_reader = 1;
|
|
switch (ggml_type) {
|
|
case FT_GGML_TYPE_Q3_K:
|
|
pn_dequantize_block_q3_k(block, out);
|
|
return;
|
|
case FT_GGML_TYPE_Q4_K:
|
|
pn_dequantize_block_q4_k(block, out);
|
|
return;
|
|
case FT_GGML_TYPE_Q5_K:
|
|
pn_dequantize_block_q5_k(block, out);
|
|
return;
|
|
case FT_GGML_TYPE_Q6_K:
|
|
pn_dequantize_block_q6_k(block, out);
|
|
return;
|
|
default:
|
|
/* Q2_K: Pantheon names the type and sizes it but ships no decoder. */
|
|
break;
|
|
}
|
|
#else
|
|
(void)ggml_type;
|
|
(void)block;
|
|
(void)out;
|
|
#endif
|
|
*has_reader = 0;
|
|
}
|
|
|
|
/**
|
|
* Decode one super-block two or three ways and require bit-exact agreement.
|
|
* Returns the number of disagreements found.
|
|
*/
|
|
static int compare_one_superblock(const kquant_kind *kind, const unsigned char *block, const char *origin,
|
|
float *ggml_out)
|
|
{
|
|
float foundry_out[SUPERBLOCK];
|
|
float pantheon_out[SUPERBLOCK];
|
|
int has_pantheon = 0;
|
|
int bad = 0;
|
|
|
|
kind->ggml_decode(block, ggml_out, SUPERBLOCK);
|
|
|
|
if (ft_gguf_dequant_to_float(kind->ggml_type, block, SUPERBLOCK, foundry_out) != 0) {
|
|
fprintf(stderr, "FAIL %s %s: foundry refused a super-block\n", kind->name, origin);
|
|
return 1;
|
|
}
|
|
pantheon_decode(kind->ggml_type, block, pantheon_out, &has_pantheon);
|
|
|
|
for (int i = 0; i < SUPERBLOCK; i++) {
|
|
if (bits_of(foundry_out[i]) != bits_of(ggml_out[i])) {
|
|
if (bad < 4)
|
|
fprintf(stderr, "FAIL %s %s [%d]: foundry %.9g (0x%08x) vs ggml %.9g (0x%08x)\n", kind->name,
|
|
origin, i, (double)foundry_out[i], bits_of(foundry_out[i]), (double)ggml_out[i],
|
|
bits_of(ggml_out[i]));
|
|
bad++;
|
|
}
|
|
if (has_pantheon && bits_of(pantheon_out[i]) != bits_of(ggml_out[i])) {
|
|
if (bad < 8)
|
|
fprintf(stderr, "FAIL %s %s [%d]: PANTHEON %.9g vs ggml %.9g\n", kind->name, origin, i,
|
|
(double)pantheon_out[i], (double)ggml_out[i]);
|
|
bad++;
|
|
}
|
|
}
|
|
return bad;
|
|
}
|
|
|
|
static void take_vector(const kquant_kind *kind, const unsigned char *block, const char *origin)
|
|
{
|
|
float ggml_out[SUPERBLOCK];
|
|
reference_vector *slot;
|
|
|
|
failures += compare_one_superblock(kind, block, origin, ggml_out);
|
|
if (vector_count >= sizeof(vectors) / sizeof(vectors[0])) {
|
|
fprintf(stderr, "FAIL: more vectors than slots\n");
|
|
failures++;
|
|
return;
|
|
}
|
|
slot = &vectors[vector_count++];
|
|
slot->ggml_type = kind->ggml_type;
|
|
snprintf(slot->origin, sizeof(slot->origin), "%s", origin);
|
|
memcpy(slot->bytes, block, kind->block_bytes);
|
|
slot->byte_count = kind->block_bytes;
|
|
for (int i = 0; i < SUPERBLOCK; i++)
|
|
slot->expected[i] = bits_of(ggml_out[i]);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Synthetic blocks */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Seeded weights for two super-blocks: [0,256) smooth, [256,512) spiky and wide. */
|
|
static void make_weights(float *weights)
|
|
{
|
|
for (int i = 0; i < SUPERBLOCK; i++)
|
|
weights[i] = 0.02f * rng_normal();
|
|
for (int i = SUPERBLOCK; i < 2 * SUPERBLOCK; i++) {
|
|
float w = 0.5f * rng_normal();
|
|
|
|
if ((rng_next() & 31u) == 0u)
|
|
w *= 16.0f;
|
|
weights[i] = w;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pseudorandom bytes with the fp16 super-block scales kept finite and ordinary:
|
|
* a random fp16 is an inf or a NaN 1 time in 32, and a NaN reference vector pins
|
|
* nothing while telling us nothing about the layout.
|
|
*/
|
|
static void make_random_block(const kquant_kind *kind, unsigned char *block)
|
|
{
|
|
for (size_t i = 0; i < kind->block_bytes; i++)
|
|
block[i] = (unsigned char)(rng_next() >> 17);
|
|
block[kind->d_offset + 1] = (unsigned char)(block[kind->d_offset + 1] & 0x1Fu);
|
|
if (kind->dmin_offset != NO_OFFSET)
|
|
block[kind->dmin_offset + 1] = (unsigned char)(block[kind->dmin_offset + 1] & 0x1Fu);
|
|
}
|
|
|
|
static void take_quantized_vectors(const kquant_kind *kind)
|
|
{
|
|
float weights[2 * SUPERBLOCK];
|
|
unsigned char blocks[2 * FT_GGML_Q6_K_BLOCK_BYTES];
|
|
char origin[192];
|
|
|
|
make_weights(weights);
|
|
kind->ggml_quantize(weights, blocks, 2 * SUPERBLOCK);
|
|
for (int b = 0; b < 2; b++) {
|
|
snprintf(origin, sizeof(origin), "synthetic: ggml quantize_row_%s_ref over seeded %s weights", kind->name,
|
|
b == 0 ? "smooth" : "spiky");
|
|
take_vector(kind, blocks + (size_t)b * kind->block_bytes, origin);
|
|
}
|
|
}
|
|
|
|
static void take_random_vectors(const kquant_kind *kind)
|
|
{
|
|
for (int v = 0; v < 2; v++) {
|
|
unsigned char block[FT_GGML_Q6_K_BLOCK_BYTES];
|
|
char origin[192];
|
|
|
|
make_random_block(kind, block);
|
|
snprintf(origin, sizeof(origin), "pseudorandom bytes, xorshift32 seed 0x%08x, draw %d", KQ_SEED, v);
|
|
take_vector(kind, block, origin);
|
|
}
|
|
}
|
|
|
|
static void take_edge_vectors(const kquant_kind *kind)
|
|
{
|
|
unsigned char block[FT_GGML_Q6_K_BLOCK_BYTES];
|
|
|
|
memset(block, 0, sizeof(block));
|
|
take_vector(kind, block, "edge: all-zero block");
|
|
|
|
memset(block, 0xff, sizeof(block));
|
|
put_half(block, kind->d_offset, 0x7bffu);
|
|
put_half(block, kind->dmin_offset, 0x7bffu);
|
|
take_vector(kind, block, "edge: every code byte 0xff, scales at the largest finite fp16 (65504)");
|
|
|
|
make_random_block(kind, block);
|
|
block[kind->d_offset + 1] = (unsigned char)(block[kind->d_offset + 1] | 0x80u);
|
|
if (kind->dmin_offset != NO_OFFSET)
|
|
block[kind->dmin_offset + 1] = (unsigned char)(block[kind->dmin_offset + 1] | 0x80u);
|
|
take_vector(kind, block, "edge: pseudorandom codes, negative fp16 scales");
|
|
|
|
make_random_block(kind, block);
|
|
put_half(block, kind->d_offset, 0x0001u);
|
|
put_half(block, kind->dmin_offset, 0x03ffu);
|
|
take_vector(kind, block, "edge: pseudorandom codes, subnormal fp16 scales (0x0001 / 0x03ff)");
|
|
}
|
|
|
|
/**
|
|
* SWEEP_BLOCKS further synthetic super-blocks per type — half quantizer output,
|
|
* half pseudorandom bytes — compared ggml against foundry and not frozen.
|
|
*/
|
|
static void sweep_kind(const kquant_kind *kind)
|
|
{
|
|
uint64_t bad = 0;
|
|
float weights[2 * SUPERBLOCK];
|
|
unsigned char blocks[2 * FT_GGML_Q6_K_BLOCK_BYTES];
|
|
|
|
for (int s = 0; s < SWEEP_BLOCKS; s += 2) {
|
|
float ggml_out[SUPERBLOCK];
|
|
|
|
if ((s / 2) % 2 == 0) {
|
|
make_weights(weights);
|
|
kind->ggml_quantize(weights, blocks, 2 * SUPERBLOCK);
|
|
} else {
|
|
make_random_block(kind, blocks);
|
|
make_random_block(kind, blocks + kind->block_bytes);
|
|
}
|
|
for (int b = 0; b < 2; b++)
|
|
bad += (uint64_t)compare_one_superblock(kind, blocks + (size_t)b * kind->block_bytes, "sweep", ggml_out);
|
|
}
|
|
printf("sweep %s: %d synthetic super-blocks, %llu mismatched words\n", kind->name, SWEEP_BLOCKS,
|
|
(unsigned long long)bad);
|
|
if (bad)
|
|
failures++;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Header emission */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void emit_preamble(FILE *f)
|
|
{
|
|
fprintf(f, "/* SPDX-License-Identifier: AGPL-3.0-or-later */\n");
|
|
fprintf(f, "/* Copyright (C) 2026 David (Corey) Herrera */\n");
|
|
fprintf(f, "/**\n");
|
|
fprintf(f, " * @file ft_gguf_kquant_reference_vectors.h\n");
|
|
fprintf(f, " * @brief GENERATED -- do not edit. K-quant super-blocks and the fp32 words\n");
|
|
fprintf(f, " * UPSTREAM GGML produced from them.\n");
|
|
fprintf(f, " *\n");
|
|
fprintf(f, " * Every block is SYNTHETIC: generated from one xorshift32 stream, seed\n");
|
|
fprintf(f, " * 0x%08x -- ggml's own quantize_row_qN_K_ref over seeded weights,\n", KQ_SEED);
|
|
fprintf(f, " * pseudorandom bytes, and edge cases (all-zero, saturated codes at the\n");
|
|
fprintf(f, " * largest finite fp16 scale, negative scales, subnormal scales). No block\n");
|
|
fprintf(f, " * comes from a model checkpoint.\n");
|
|
fprintf(f, " *\n");
|
|
fprintf(f, " * Every `expected` word below came out of ggml's own compiled\n");
|
|
fprintf(f, " * dequantize_row_qN_K (libggml-base), not out of foundry and not out of a\n");
|
|
fprintf(f, " * transcription of ggml. test_convert.c requires ft_gguf_dequant.c to\n");
|
|
fprintf(f, " * reproduce all of them BIT-EXACTLY.\n");
|
|
fprintf(f, " *\n");
|
|
fprintf(f, " * Regenerate, from tt/convert:\n");
|
|
fprintf(f, " * reference/generate_kquant_reference.sh <llama.cpp checkout>\n");
|
|
fprintf(f, " * (the checkout must have build/bin/libggml-base). Committed so the test\n");
|
|
fprintf(f, " * needs no ggml.\n");
|
|
fprintf(f, " *\n");
|
|
fprintf(f, " * @status None\n");
|
|
fprintf(f, " * @issues None\n");
|
|
fprintf(f, " * @todo None\n");
|
|
fprintf(f, " */\n\n");
|
|
fprintf(f, "#ifndef FT_GGUF_KQUANT_REFERENCE_VECTORS_H\n");
|
|
fprintf(f, "#define FT_GGUF_KQUANT_REFERENCE_VECTORS_H\n\n");
|
|
fprintf(f, "#include <stdint.h>\n\n");
|
|
fprintf(f, "#define FT_KQUANT_REF_COUNT %zu\n", vector_count);
|
|
fprintf(f, "#define FT_KQUANT_REF_VALUES 256\n");
|
|
fprintf(f, "#define FT_KQUANT_REF_MAX_BYTES %d\n\n", FT_GGML_Q6_K_BLOCK_BYTES);
|
|
fprintf(f, "typedef struct {\n");
|
|
fprintf(f, " uint32_t ggml_type;\n");
|
|
fprintf(f, " const char *origin;\n");
|
|
fprintf(f, " uint32_t byte_count;\n");
|
|
fprintf(f, " const unsigned char *bytes;\n");
|
|
fprintf(f, " const uint32_t *expected_bits;\n");
|
|
fprintf(f, "} ft_kquant_reference_vector;\n\n");
|
|
}
|
|
|
|
static void emit_header(const char *out_path)
|
|
{
|
|
FILE *f = fopen(out_path, "w");
|
|
|
|
if (!f) {
|
|
fprintf(stderr, "cannot write %s\n", out_path);
|
|
failures++;
|
|
return;
|
|
}
|
|
emit_preamble(f);
|
|
|
|
for (size_t v = 0; v < vector_count; v++) {
|
|
fprintf(f, "static const unsigned char ft_kquant_ref_bytes_%zu[%zu] = {\n", v, vectors[v].byte_count);
|
|
for (size_t i = 0; i < vectors[v].byte_count; i++)
|
|
fprintf(f, "%s0x%02xu%s", i % 12 == 0 ? " " : " ", vectors[v].bytes[i],
|
|
i + 1 == vectors[v].byte_count ? "\n" : (i % 12 == 11 ? ",\n" : ","));
|
|
fprintf(f, "};\n\n");
|
|
|
|
fprintf(f, "static const uint32_t ft_kquant_ref_expected_%zu[256] = {\n", v);
|
|
for (int i = 0; i < SUPERBLOCK; i++)
|
|
fprintf(f, "%s0x%08xu%s", i % 8 == 0 ? " " : " ", vectors[v].expected[i],
|
|
i + 1 == SUPERBLOCK ? "\n" : (i % 8 == 7 ? ",\n" : ","));
|
|
fprintf(f, "};\n\n");
|
|
}
|
|
|
|
fprintf(f, "static const ft_kquant_reference_vector ft_kquant_reference_vectors[FT_KQUANT_REF_COUNT] = {\n");
|
|
for (size_t v = 0; v < vector_count; v++)
|
|
fprintf(f, " {%u, \"%s\", %u, ft_kquant_ref_bytes_%zu, ft_kquant_ref_expected_%zu},\n",
|
|
vectors[v].ggml_type, vectors[v].origin, (unsigned)vectors[v].byte_count, v, v);
|
|
fprintf(f, "};\n\n");
|
|
fprintf(f, "#endif /* FT_GGUF_KQUANT_REFERENCE_VECTORS_H */\n");
|
|
|
|
fclose(f);
|
|
printf("wrote %s: %zu vectors\n", out_path, vector_count);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (argc < 2) {
|
|
fprintf(stderr, "usage: %s <out.h>\n", argv[0]);
|
|
return 2;
|
|
}
|
|
|
|
for (size_t k = 0; k < KQUANT_KINDS; k++) {
|
|
take_quantized_vectors(&KINDS[k]);
|
|
take_random_vectors(&KINDS[k]);
|
|
take_edge_vectors(&KINDS[k]);
|
|
}
|
|
|
|
printf("frozen vectors:\n");
|
|
for (size_t v = 0; v < vector_count; v++) {
|
|
const char *name = "?";
|
|
|
|
for (size_t k = 0; k < KQUANT_KINDS; k++) {
|
|
if (KINDS[k].ggml_type == vectors[v].ggml_type)
|
|
name = KINDS[k].name;
|
|
}
|
|
printf(" %-5s %s\n", name, vectors[v].origin);
|
|
}
|
|
printf("\n");
|
|
|
|
for (size_t k = 0; k < KQUANT_KINDS; k++)
|
|
sweep_kind(&KINDS[k]);
|
|
|
|
#ifdef PANTHEON
|
|
printf("Pantheon's reader cross-checked (Q3_K..Q6_K)\n");
|
|
#else
|
|
printf("Pantheon's reader not cross-checked (PANTHEON_DIR unset)\n");
|
|
#endif
|
|
|
|
if (failures == 0)
|
|
emit_header(argv[1]);
|
|
printf("\n%d failures\n", failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|