semq-sys 1.1.0

Raw FFI bindings to the SEMQ native library.
Documentation
/*
 * Copyright (c) 2026 The SEMQ Group Inc.
 * Licensed under the PolyForm Noncommercial License 1.0.0. See LICENSE.md for terms.
 */

/* Contextual noise envelopes and exact integer verdicts. */
#include "semq_internal.h"
#include <stdlib.h>

static const char RESERVED_ENCODER[]  = "encoder";
static const char RESERVED_REVISION[] = "encoder_revision";

int semqi_mul_le(uint64_t a, uint64_t b, uint64_t c, uint64_t d) {
    /* 64x64 -> 128 via 32-bit limbs; compare (hi, lo) pairs. */
    const uint64_t a0 = a & 0xFFFFFFFFu, a1 = a >> 32, b0 = b & 0xFFFFFFFFu, b1 = b >> 32;
    const uint64_t c0 = c & 0xFFFFFFFFu, c1 = c >> 32, d0 = d & 0xFFFFFFFFu, d1 = d >> 32;
    uint64_t lo, hi, lo2, hi2;
    {
        const uint64_t p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
        const uint64_t mid = (p00 >> 32) + (p01 & 0xFFFFFFFFu) + (p10 & 0xFFFFFFFFu);
        lo = (p00 & 0xFFFFFFFFu) | (mid << 32);
        hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
    }
    {
        const uint64_t p00 = c0 * d0, p01 = c0 * d1, p10 = c1 * d0, p11 = c1 * d1;
        const uint64_t mid = (p00 >> 32) + (p01 & 0xFFFFFFFFu) + (p10 & 0xFFFFFFFFu);
        lo2 = (p00 & 0xFFFFFFFFu) | (mid << 32);
        hi2 = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
    }
    if (hi != hi2) return hi < hi2;
    return lo <= lo2;
}

static int cmp_u64(const void* a, const void* b) {
    const uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
    return (x < y) ? -1 : (x > y) ? 1 : 0;
}

/* Nearest-rank p99: 0 for m = 0, otherwise the k-th smallest (1-based)
 * with k = m - floor(m / 100). */
static semq_status_t p99(const uint64_t* values, uint64_t m, uint64_t* out, semq_error_t* err) {
    if (m == 0u) { *out = 0u; return SEMQ_OK; }
    uint64_t* tmp = (uint64_t*)semqi_alloc(m * sizeof(uint64_t));
    if (tmp == NULL) return SEMQI_NOMEM(err);
    memcpy(tmp, values, (size_t)m * sizeof(uint64_t));
    qsort(tmp, (size_t)m, sizeof(uint64_t), cmp_u64);
    const uint64_t k = m - m / 100u;
    *out = tmp[k - 1u];
    semqi_free(tmp);
    return SEMQ_OK;
}

static int is_reserved(const semq_manifest_change_t* m) {
    return (m->key_len == sizeof(RESERVED_ENCODER) - 1u && memcmp(m->key, RESERVED_ENCODER, m->key_len) == 0)
        || (m->key_len == sizeof(RESERVED_REVISION) - 1u && memcmp(m->key, RESERVED_REVISION, m->key_len) == 0);
}

static int has_reserved_change(const semq_diff_t* d) {
    for (uint32_t m = 0u; m < d->n_mchanges; m++) {
        if (is_reserved(&d->mchanges[m])) return 1;
    }
    return 0;
}

/* Reference state_id of a diff: the second half of the reference footer. */
static const uint8_t* reference_state_id(const semq_diff_t* d) {
    return d->ref->footer + SEMQ_DIGEST_BYTES;
}

semq_status_t semqi_floor_create(const semq_config_t* config, uint32_t id_kind,
                                 const uint8_t reference_id[32], uint64_t nulls,
                                 uint64_t changed_rows, uint64_t total_rows,
                                 uint64_t hamming, uint64_t max_hamming, uint64_t distinct_nulls,
                                 semq_floor_t** out, semq_error_t* err) {
    if (out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    *out = NULL;
    if (config == NULL || reference_id == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "NULL argument");
    semq_status_t s = semq_config_validate(config, err);
    if (s != SEMQ_OK) return s;
    if (id_kind != (uint32_t)SEMQ_ID_U64 && id_kind != (uint32_t)SEMQ_ID_UTF8) {
        return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.id_kind must be u64 or utf8");
    }
    if (nulls == 0u) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.nulls must be > 0");
    if (total_rows == 0u) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.total_rows must be > 0");
    if (changed_rows > total_rows) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.changed_rows exceeds total_rows");
    if (hamming > (uint64_t)semq_config_units_per_row(config)) {
        return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.hamming exceeds units_per_row");
    }
    if (max_hamming != SEMQ_NONE) {
        if (max_hamming < hamming) {
            return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.max_hamming is below floor.hamming");
        }
        if (max_hamming > (uint64_t)semq_config_units_per_row(config)) {
            return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.max_hamming exceeds units_per_row");
        }
    }
    if (distinct_nulls != SEMQ_NONE) {
        if (distinct_nulls == 0u) {
            return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.distinct_nulls must be at least 1");
        }
        if (distinct_nulls > nulls) {
            return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "floor.distinct_nulls exceeds floor.nulls");
        }
    }
    semq_floor_t* f = (semq_floor_t*)semqi_alloc(sizeof(*f));
    if (f == NULL) return SEMQI_NOMEM(err);
    f->config       = *config;
    f->id_kind      = id_kind;
    memcpy(f->reference_id, reference_id, SEMQ_DIGEST_BYTES);
    f->nulls        = nulls;
    f->changed_rows = changed_rows;
    f->total_rows   = total_rows;
    f->hamming      = hamming;
    f->max_hamming  = max_hamming;
    f->distinct_nulls = distinct_nulls;
    *out = f;
    semqi_ok(err);
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_floor_create(const semq_config_t* config, uint32_t id_kind,
                                         const uint8_t reference_id[32], uint64_t nulls,
                                         uint64_t changed_rows, uint64_t total_rows,
                                         uint64_t hamming, semq_floor_t** out,
                                         semq_error_t* err) {
    return semqi_floor_create(config, id_kind, reference_id, nulls, changed_rows, total_rows, hamming,
                              SEMQ_NONE, SEMQ_NONE, out, err);
}

SEMQ_API void semq_floor_free(semq_floor_t* f) { semqi_free(f); }

SEMQ_API const semq_config_t* semq_floor_config(const semq_floor_t* f) { return &f->config; }
SEMQ_API uint32_t semq_floor_id_kind(const semq_floor_t* f) { return f->id_kind; }
SEMQ_API void semq_floor_reference_id(const semq_floor_t* f, uint8_t out[32]) {
    memcpy(out, f->reference_id, SEMQ_DIGEST_BYTES);
}
SEMQ_API uint64_t semq_floor_nulls(const semq_floor_t* f) { return f->nulls; }
SEMQ_API uint64_t semq_floor_changed_rows(const semq_floor_t* f) { return f->changed_rows; }
SEMQ_API uint64_t semq_floor_total_rows(const semq_floor_t* f) { return f->total_rows; }
SEMQ_API uint64_t semq_floor_hamming(const semq_floor_t* f) { return f->hamming; }
SEMQ_API uint64_t semq_floor_max_hamming(const semq_floor_t* f) { return f->max_hamming; }
SEMQ_API uint64_t semq_floor_distinct_nulls(const semq_floor_t* f) { return f->distinct_nulls; }

/* Every check this version defines. */
#define KNOWN_CHECKS ((uint32_t)SEMQ_CHECK_PER_ROW)

/* The checks of a verdict as reason flags, all of them evaluated. With
 * `per_row`, also the rows above max_hamming, into a new array in `*rows`
 * (NULL when there are none). The floor's context is checked first. */
static semq_status_t gate(const semq_diff_t* d, const semq_floor_t* f, int per_row, uint32_t* reasons,
                          uint64_t** rows, uint64_t* n_rows, semq_error_t* err) {
    if (!semq_config_equal(&f->config, &d->ref->config)) {
        return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, SEMQ_NONE, "floor was measured with a different config");
    }
    if (f->id_kind != (uint32_t)d->ref->id_kind) {
        return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, SEMQ_NONE, "floor was measured with a different id kind");
    }
    if (memcmp(f->reference_id, reference_state_id(d), SEMQ_DIGEST_BYTES) != 0) {
        return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, SEMQ_NONE, "floor was measured against a different reference");
    }
    if (per_row && f->max_hamming == SEMQ_NONE) {
        return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, SEMQ_NONE,
                          "floor has no per-row data (max_hamming); measure it again with the per-row check");
    }
    const uint64_t n_common = d->n_unchanged + d->n_changed;
    uint64_t p = 0u;
    const semq_status_t s = p99(d->hamming, d->n_changed, &p, err);
    if (s != SEMQ_OK) return s;
    uint32_t r = 0u;
    if (n_common == 0u) r |= (uint32_t)SEMQ_REASON_NO_COMMON_ROWS;
    if (d->n_removed != 0u) r |= (uint32_t)SEMQ_REASON_REMOVED_ROWS;
    if (!semqi_mul_le(d->n_changed, f->total_rows, f->changed_rows, n_common)) r |= (uint32_t)SEMQ_REASON_CHANGED_RATIO;
    if (p > f->hamming) r |= (uint32_t)SEMQ_REASON_HAMMING;
    if (has_reserved_change(d)) r |= (uint32_t)SEMQ_REASON_ENCODER;
    uint64_t count = 0u;
    if (per_row) {
        for (uint64_t i = 0u; i < d->n_changed; i++) {
            if (d->hamming[i] > f->max_hamming) count++;
        }
    }
    if (count > 0u) {
        uint64_t bytes = 0u;
        if (semqi_mul_ovf(count, sizeof(uint64_t), &bytes)) return SEMQI_NOMEM(err);
        uint64_t* list = (uint64_t*)semqi_alloc(bytes);
        if (list == NULL) return SEMQI_NOMEM(err);
        uint64_t j = 0u;
        for (uint64_t i = 0u; i < d->n_changed; i++) {
            if (d->hamming[i] > f->max_hamming) list[j++] = i;
        }
        r |= (uint32_t)SEMQ_REASON_ROW_ABOVE_MAX;
        *rows = list;
    }
    *n_rows = count;
    *reasons = r;
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_diff_within(const semq_diff_t* d, const semq_floor_t* f, int* out,
                                        semq_error_t* err) {
    if (d == NULL || f == NULL || out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "NULL argument");
    uint32_t reasons = 0u;
    uint64_t* rows = NULL;
    uint64_t n_rows = 0u;
    const semq_status_t s = gate(d, f, 0, &reasons, &rows, &n_rows, err);
    if (s != SEMQ_OK) return s;
    *out = reasons == 0u;
    semqi_ok(err);
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_diff_evaluate(const semq_diff_t* d, const semq_floor_t* f, uint32_t checks,
                                          semq_verdict_t** out, semq_error_t* err) {
    if (out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    *out = NULL;
    if (d == NULL || f == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "NULL argument");
    if ((checks & ~KNOWN_CHECKS) != 0u) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "checks has an unknown bit");
    semq_verdict_t* v = (semq_verdict_t*)semqi_calloc(1u, sizeof(*v));
    if (v == NULL) return SEMQI_NOMEM(err);
    const semq_status_t s = gate(d, f, (checks & (uint32_t)SEMQ_CHECK_PER_ROW) != 0u, &v->reasons, &v->rows,
                                 &v->n_rows, err);
    if (s != SEMQ_OK) {
        semq_verdict_free(v);
        return s;
    }
    *out = v;
    semqi_ok(err);
    return SEMQ_OK;
}

SEMQ_API void semq_verdict_free(semq_verdict_t* v) {
    if (v == NULL) return;
    semqi_free(v->rows);
    semqi_free(v);
}

SEMQ_API int semq_verdict_passed(const semq_verdict_t* v) { return v->reasons == 0u; }
SEMQ_API uint32_t semq_verdict_reasons(const semq_verdict_t* v) { return v->reasons; }
SEMQ_API uint64_t semq_verdict_row_count(const semq_verdict_t* v) { return v->n_rows; }
SEMQ_API uint64_t semq_verdict_row(const semq_verdict_t* v, uint64_t i) {
    return i < v->n_rows ? v->rows[i] : SEMQ_NONE;
}

static int cmp_digest(const void* a, const void* b) {
    return memcmp(*(const uint8_t* const*)a, *(const uint8_t* const*)b, SEMQ_DIGEST_BYTES);
}

/* How many distinct candidate content_digests the nulls have. */
static semq_status_t count_distinct(const semq_diff_t* const* diffs, uint32_t k, uint64_t* out, semq_error_t* err) {
    uint64_t bytes = 0u;
    if (semqi_mul_ovf(k, sizeof(const uint8_t*), &bytes)) return SEMQI_NOMEM(err);
    const uint8_t** digests = (const uint8_t**)semqi_alloc(bytes);
    if (digests == NULL) return SEMQI_NOMEM(err);
    for (uint32_t i = 0u; i < k; i++) digests[i] = diffs[i]->cand->footer;  /* content_digest */
    qsort((void*)digests, (size_t)k, sizeof(digests[0]), cmp_digest);
    uint64_t n = 1u;
    for (uint32_t i = 1u; i < k; i++) {
        if (memcmp(digests[i - 1u], digests[i], SEMQ_DIGEST_BYTES) != 0) n++;
    }
    semqi_free((void*)digests);
    *out = n;
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_floor_measure_for(const semq_diff_t* const* diffs, uint32_t k, uint32_t checks,
                                              semq_floor_t** out, semq_error_t* err) {
    if (out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    *out = NULL;
    if ((checks & ~KNOWN_CHECKS) != 0u) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "checks has an unknown bit");
    if (diffs == NULL || k == 0u) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "measure needs at least one null diff");
    const int per_row = (checks & (uint32_t)SEMQ_CHECK_PER_ROW) != 0u;
    uint64_t best_changed = 0u, best_common = 1u, max_p = 0u, max_row = 0u;
    for (uint32_t i = 0u; i < k; i++) {
        const semq_diff_t* d = diffs[i];
        if (d == NULL) return SEMQI_INVALID(err, SEMQ_NONE, i, "null diff is NULL");
        if (!semq_config_equal(&d->ref->config, &diffs[0]->ref->config)) {
            return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, i, "null diffs have different configs");
        }
        if (d->ref->id_kind != diffs[0]->ref->id_kind) {
            return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, i, "null diffs have different id kinds");
        }
        if (memcmp(reference_state_id(d), reference_state_id(diffs[0]), SEMQ_DIGEST_BYTES) != 0) {
            return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, i, "null diffs have different references");
        }
        const uint64_t n_common = d->n_unchanged + d->n_changed;
        if (n_common == 0u) return SEMQI_INVALID(err, SEMQ_NONE, i, "null diff has no rows in common");
        if (d->n_added != 0u || d->n_removed != 0u) {
            return SEMQI_INVALID(err, SEMQ_NONE, i, "null diff adds or removes rows");
        }
        if (has_reserved_change(d)) {
            return SEMQI_INVALID(err, SEMQ_NONE, i, "null diff changes encoder or encoder_revision");
        }
        /* Largest ratio changed / common, compared exactly. */
        if (i == 0u || !semqi_mul_le(d->n_changed, best_common, best_changed, n_common)) {
            best_changed = d->n_changed;
            best_common  = n_common;
        }
        uint64_t p = 0u;
        const semq_status_t s = p99(d->hamming, d->n_changed, &p, err);
        if (s != SEMQ_OK) return s;
        if (p > max_p) max_p = p;
        if (per_row) {
            for (uint64_t r = 0u; r < d->n_changed; r++) {
                if (d->hamming[r] > max_row) max_row = d->hamming[r];
            }
        }
    }
    uint64_t distinct = SEMQ_NONE;
    if (per_row) {
        const semq_status_t s = count_distinct(diffs, k, &distinct, err);
        if (s != SEMQ_OK) return s;
    }
    return semqi_floor_create(&diffs[0]->ref->config, (uint32_t)diffs[0]->ref->id_kind,
                              reference_state_id(diffs[0]), k, best_changed, best_common, max_p,
                              per_row ? max_row : SEMQ_NONE, distinct, out, err);
}

SEMQ_API semq_status_t semq_floor_measure(const semq_diff_t* const* diffs, uint32_t k,
                                          semq_floor_t** out, semq_error_t* err) {
    return semq_floor_measure_for(diffs, k, 0u, out, err);
}