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.
 */

/*
 * semq_codec.c — Codec: the input contract and the operator kernels.
 *
 * encode applies, per row: subnormal canonicalization, finiteness, the
 * unit-norm check, then the operator's symbol mapping through the kernel
 * selected for this host. decode and unpack are the inverse views of a
 * canonical row.
 */

#include <fenv.h>
#include <float.h>
#include <math.h>
#include <stdlib.h>

#include "semq_internal.h"
#include "semq_selftest_vectors.h"
#include "semq_sha256.h"

/* orbit kernels emit the 30-symbol layout (sign * 10 + digit); the
 * canonical alphabet is the dense 19-symbol one. Both maps are bijective
 * on the reachable symbols. */
static const uint8_t SPARSE_TO_DENSE[30] = {
     0, 10, 11, 12, 13, 14, 15, 16, 17, 18,
     0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
     0,  1,  2,  3,  4,  5,  6,  7,  8,  9,
};
static const uint8_t DENSE_TO_SPARSE[19] = {
    10, 21, 22, 23, 24, 25, 26, 27, 28, 29, 1, 2, 3, 4, 5, 6, 7, 8, 9,
};

static semq_status_t kernel_encode(const semq_codec_t* c, const float* row, uint8_t* out);

/* Fill a codec for `cfg` with the kernels of `kind`. */
static void codec_init(semq_codec_t* c, const semq_config_t* cfg, semq_backend_kind_t kind) {
    c->config        = *cfg;
    c->bpv           = semq_config_bytes_per_vector(cfg);
    c->units         = semq_config_units_per_row(cfg);
    c->phase_packed  = (cfg->op == SEMQ_PHASE && cfg->p1 <= 16u) ? 1u : 0u;
    c->max_magnitude = semq_config_max_magnitude(cfg);
    c->kind          = kind;
    c->orbit         = semq_dispatch_get(kind);
    c->phase         = semq_dispatch_get_phase(kind);
    c->quant         = semq_dispatch_get_quant(kind);
    if (cfg->op == SEMQ_PHASE) {
        for (uint32_t sct = 0u; sct < cfg->p1; sct++) {
            double rep[2];
            semq_phase_representative(cfg->p1, sct, rep);
            c->phase_rep[2u * sct + 0u] = (float)rep[0];
            c->phase_rep[2u * sct + 1u] = (float)rep[1];
        }
    }
}

/* -------------------------------------------------------------------------- */
/*  Known-answer self-test                                                    */
/* -------------------------------------------------------------------------- */

#ifdef SEMQ_TEST_ALLOCATOR
static uint32_t corrupt_self_test;
void semqi_test_corrupt_self_test(uint32_t on) { corrupt_self_test = on; } /* NOLINT(misc-use-internal-linkage): tests/fault calls it */
#endif

/* Before a codec exists, encode fixed rows with the kernels selected for this
 * host and compare the bytes with the scalar kernel's (semq_selftest_vectors.h,
 * generated by tools/gen_selftest.py), and hash a known message. Rust and Go
 * compile this core with the user's compiler, so a build or CPU that breaks
 * determinism is refused here instead of producing other bytes. It allocates
 * nothing and costs a few microseconds per codec. */
static semq_status_t self_test(semq_backend_kind_t kind, uint32_t op, semq_error_t* err) {
    static const uint8_t ABC_SHA256[32] = {
        0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
        0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
    };
    uint8_t digest[32];
    semq_sha256_ctx_t h;
    semq_sha256_init(&h);
    semq_sha256_update(&h, (const uint8_t*)"abc", 3u);
    semq_sha256_final(&h, digest);
    if (memcmp(digest, ABC_SHA256, sizeof(digest)) != 0) {
        return semqi_fail(err, SEMQ_ERR_INTERNAL, SEMQ_NONE, SEMQ_NONE,
                          "self-test failed: SHA-256 gives a wrong digest on this build");
    }
    for (size_t k = 0u; k < sizeof(SEMQI_KAT) / sizeof(SEMQI_KAT[0]); k++) {
        const semqi_kat_t* kat = &SEMQI_KAT[k];
        if (kat->op != op) continue;
        semq_config_t cfg;
        semq_status_t s = (op == SEMQ_ORBIT)   ? semq_config_orbit(kat->dim, kat->p1, &cfg, NULL)
                          : (op == SEMQ_PHASE) ? semq_config_phase(kat->dim, kat->p1, &cfg, NULL)
                                               : semq_config_quant(kat->dim, kat->p1, &cfg, NULL);
        if (s != SEMQ_OK || kat->dim > 64u || semq_config_bytes_per_vector(&cfg) != kat->bytes_per_vector) {
            return semqi_fail(err, SEMQ_ERR_INTERNAL, SEMQ_NONE, SEMQ_NONE,
                              "self-test failed: a known-answer config does not match this core");
        }
        semq_codec_t t;
        memset(&t, 0, sizeof(t));
        codec_init(&t, &cfg, kind);
        for (uint32_t r = 0u; r < SEMQI_KAT_ROWS; r++) {
            float row[64];
            uint8_t got[64];
            for (uint32_t i = 0u; i < kat->dim; i++) {
                const uint32_t bits = kat->input[(size_t)r * kat->dim + i];
                memcpy(&row[i], &bits, sizeof(bits));
            }
            s = kernel_encode(&t, row, got);
            const uint8_t* want = kat->expected + (size_t)r * kat->bytes_per_vector;
            int differs = s != SEMQ_OK || memcmp(got, want, kat->bytes_per_vector) != 0;
#ifdef SEMQ_TEST_ALLOCATOR
            differs |= corrupt_self_test != 0u;
#endif
            if (differs) {
                return semqi_fail(err, SEMQ_ERR_INTERNAL, SEMQ_NONE, SEMQ_NONE,
                                  "self-test failed: this build's kernel does not produce the "
                                  "reference bytes; the core is miscompiled or the CPU is faulty");
            }
        }
    }
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_codec_create(const semq_config_t* cfg, semq_codec_t** out,
                                         semq_error_t* err) {
    if (out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    *out = NULL;
    semq_status_t s = semq_config_validate(cfg, err);
    if (s != SEMQ_OK) return s;
    /* Cached FP values are part of the effective codec. Admit the environment
     * before deriving them, even if encode later runs under another mode. */
    if (fegetround() != FE_TONEAREST) {
        return semqi_fail(err, SEMQ_ERR_UNSUPPORTED, SEMQ_NONE, SEMQ_NONE,
                          "floating-point rounding mode is not round-to-nearest");
    }
    const semq_backend_kind_t kind = semq_dispatch_select_for(cfg->op);
    s = self_test(kind, cfg->op, err);
    if (s != SEMQ_OK) return s;
    semq_codec_t* c = (semq_codec_t*)semqi_calloc(1u, sizeof(*c));
    if (c == NULL) return SEMQI_NOMEM(err);
    codec_init(c, cfg, kind);
    *out = c;
    semqi_ok(err);
    return SEMQ_OK;
}

SEMQ_API void semq_codec_free(semq_codec_t* codec) {
    if (codec == NULL) return;
    memset(codec, 0, sizeof(*codec));
    semqi_free(codec);
}

SEMQ_API const semq_config_t* semq_codec_config(const semq_codec_t* codec) {
    return codec == NULL ? NULL : &codec->config;
}


SEMQ_API const char* semq_codec_backend(const semq_codec_t* codec) {
    if (codec == NULL) return "none";
    /* The kind was selected when the codec was created; report that one,
     * not what the dispatcher would pick now. */
    return semqi_backend_name_for(codec->kind, codec->config.op);
}

/* -------------------------------------------------------------------------- */
/*  Input contract                                                            */
/* -------------------------------------------------------------------------- */

/* Copy one row into `scratch`, replacing every subnormal and both zeros by
 * +0.0, then check finiteness and the unit norm. `row` is the input row
 * index reported in errors. */
static semq_status_t admit_row(const float* src, uint32_t dim, float* scratch, uint64_t row,
                               semq_error_t* err) {
    for (uint32_t i = 0u; i < dim; i++) {
        const float x = src[i];
        /* fabsf(NaN) < FLT_MIN is false, so NaN survives to the next check. */
        scratch[i] = (fabsf(x) < FLT_MIN) ? 0.0f : x;
    }
    for (uint32_t i = 0u; i < dim; i++) {
        if (!isfinite(scratch[i])) {
            return SEMQI_INVALID(err, row, i, "vector has a NaN or infinite coordinate");
        }
    }
    double s = 0.0;
    for (uint32_t i = 0u; i < dim; i++) {
        const double x = (double)scratch[i];
        s += x * x;
    }
    const double dev = s - 1.0;
    if (!(dev <= 0.0009765625 && dev >= -0.0009765625)) {
        return SEMQI_INVALID(err, row, SEMQ_NONE,
                             "vector is not unit-norm (|sum of squares - 1| > 2^-10)");
    }
    return SEMQ_OK;
}

static semq_status_t kernel_encode(const semq_codec_t* c, const float* row, uint8_t* out) {
    switch (c->config.op) {
        case SEMQ_ORBIT: {
            const semq_status_t s = c->orbit->encode(row, c->config.dim, c->config.p1, out);
            if (s != SEMQ_OK) return s;
            for (uint32_t i = 0u; i < c->config.dim; i++) out[i] = SPARSE_TO_DENSE[out[i]];
            return SEMQ_OK;
        }
        case SEMQ_PHASE:
            return c->phase->encode(row, c->config.dim, c->config.p1, c->phase_packed, out);
        default:
            return c->quant->encode(row, c->config.dim, c->config.p1, c->max_magnitude, out);
    }
}

SEMQ_API semq_status_t semq_codec_encode(const semq_codec_t* codec, const semq_ids_t* ids,
                                         const float* vectors, const semq_pair_t* manifest,
                                         uint32_t n_pairs, semq_encoding_t** out,
                                         semq_error_t* err) {
    if (out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    *out = NULL;
    if (codec == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "codec is NULL");
    if (ids == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "ids is NULL");
    if (ids->n > 0u && vectors == NULL) {
        return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "vectors is NULL");
    }
    if (fegetround() != FE_TONEAREST) {
        return semqi_fail(err, SEMQ_ERR_UNSUPPORTED, SEMQ_NONE, SEMQ_NONE,
                          "floating-point rounding mode is not round-to-nearest");
    }

    uint64_t input_bytes;
    if (semqi_mul_ovf(ids->n, (uint64_t)codec->config.dim * sizeof(float), &input_bytes) ||
        semqi_too_big(input_bytes)) return SEMQI_NOMEM(err);
    uint64_t* perm = NULL;
    semq_status_t s = semqi_sort_ids(ids, &perm, err);
    if (s != SEMQ_OK) return s;

    const uint32_t dim = codec->config.dim;
    uint64_t rows_len;
    if (semqi_mul_ovf(ids->n, codec->bpv, &rows_len) || semqi_too_big(rows_len)) {
        semqi_free(perm);
        return SEMQI_NOMEM(err);
    }
    uint8_t* rows    = (uint8_t*)semqi_alloc(rows_len);
    float*   scratch = (float*)semqi_alloc((uint64_t)dim * sizeof(float));
    if (rows == NULL || scratch == NULL) {
        semqi_free(perm); semqi_free(rows); semqi_free(scratch);
        return SEMQI_NOMEM(err);
    }

    for (uint64_t i = 0u; i < ids->n; i++) {
        const uint64_t src_row = perm[i];
        s = admit_row(vectors + (size_t)src_row * dim, dim, scratch, src_row, err);
        if (s != SEMQ_OK) goto fail;
        s = kernel_encode(codec, scratch, rows + (size_t)i * codec->bpv);
        if (s != SEMQ_OK) {
            s = semqi_fail(err, SEMQ_ERR_INTERNAL, src_row, SEMQ_NONE, "kernel rejected an admitted row");
            goto fail;
        }
    }

    {
        semq_encoding_t* enc = semqi_encoding_alloc();
        if (enc == NULL) { s = SEMQI_NOMEM(err); goto fail; }
        enc->config  = codec->config;
        enc->bpv     = codec->bpv;
        enc->units   = codec->units;
        enc->id_kind = ids->kind;
        enc->n       = ids->n;
        enc->rows    = rows;
        enc->rows_len = rows_len;
        rows = NULL;
        s = semqi_ids_section(ids, perm, &enc->ids, &enc->ids_len, err);
        if (s == SEMQ_OK) s = semqi_manifest_build(manifest, n_pairs, &enc->manifest, &enc->manifest_len, err);
        if (s == SEMQ_OK) s = semqi_encoding_finish(enc, err);
        if (s != SEMQ_OK) { semq_encoding_free(enc); goto fail; }
        *out = enc;
    }
    semqi_free(perm);
    semqi_free(scratch);
    semqi_ok(err);
    return SEMQ_OK;

fail:
    semqi_free(perm);
    semqi_free(rows);
    semqi_free(scratch);
    return s;
}

/* -------------------------------------------------------------------------- */
/*  decode / unpack                                                           */
/* -------------------------------------------------------------------------- */

static semq_status_t check_pair(const semq_codec_t* codec, const semq_encoding_t* enc,
                                const void* out, semq_error_t* err) {
    if (codec == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "codec is NULL");
    if (enc == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "encoding is NULL");
    if (enc->n > 0u && out == NULL) return SEMQI_INVALID(err, SEMQ_NONE, SEMQ_NONE, "output is NULL");
    if (!semq_config_equal(&codec->config, &enc->config)) {
        return semqi_fail(err, SEMQ_ERR_INCOMPATIBLE, SEMQ_NONE, SEMQ_NONE,
                          "encoding config differs from codec config");
    }
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_codec_decode(const semq_codec_t* codec, const semq_encoding_t* enc,
                                         float* out, semq_error_t* err) {
    semq_status_t s = check_pair(codec, enc, out, err);
    if (s != SEMQ_OK) return s;
    const uint32_t dim = codec->config.dim;
    if (codec->config.op == SEMQ_PHASE) {
        /* Representatives are outside the byte contract; they come from the
         * per-sector table built at creation, which equals the scalar
         * inverse for every pair. Rows are canonical, so every sector is
         * below the sector count. */
        const uint32_t pairs = dim / 2u;
        for (uint64_t i = 0u; i < enc->n; i++) {
            const uint8_t* row = enc->rows + (size_t)i * codec->bpv;
            float* dst = out + (size_t)i * dim;
            for (uint32_t p = 0u; p < pairs; p++) {
                const uint32_t sct = codec->phase_packed
                    ? (((uint32_t)row[p >> 1] >> (4u * (p & 1u))) & 0x0Fu)
                    : (uint32_t)row[p];
                dst[2u * p + 0u] = codec->phase_rep[2u * sct + 0u];
                dst[2u * p + 1u] = codec->phase_rep[2u * sct + 1u];
            }
        }
        semqi_ok(err);
        return SEMQ_OK;
    }
    double*  tmp    = (double*)semqi_alloc((uint64_t)dim * sizeof(double));
    uint8_t* sparse = (uint8_t*)semqi_alloc(codec->config.op == SEMQ_ORBIT ? dim : 1u);
    if (tmp == NULL || sparse == NULL) { semqi_free(tmp); semqi_free(sparse); return SEMQI_NOMEM(err); }
    for (uint64_t i = 0u; i < enc->n; i++) {
        const uint8_t* row = enc->rows + (size_t)i * codec->bpv;
        semq_status_t k;
        switch (codec->config.op) {
            case SEMQ_ORBIT:
                for (uint32_t j = 0u; j < dim; j++) sparse[j] = DENSE_TO_SPARSE[row[j]];
                k = codec->orbit->reconstruct(sparse, dim, codec->config.p1, tmp);
                break;
            default:
                k = codec->quant->reconstruct(row, dim, codec->config.p1, codec->max_magnitude, tmp);
                break;
        }
        if (k != SEMQ_OK) {
            semqi_free(tmp); semqi_free(sparse);
            return semqi_fail(err, SEMQ_ERR_INTERNAL, i, SEMQ_NONE, "kernel rejected a canonical row");
        }
        float* dst = out + (size_t)i * dim;
        for (uint32_t j = 0u; j < dim; j++) dst[j] = (float)tmp[j];
    }
    semqi_free(tmp);
    semqi_free(sparse);
    semqi_ok(err);
    return SEMQ_OK;
}

SEMQ_API semq_status_t semq_codec_unpack(const semq_codec_t* codec, const semq_encoding_t* enc,
                                         uint8_t* out, semq_error_t* err) {
    semq_status_t s = check_pair(codec, enc, out, err);
    if (s != SEMQ_OK) return s;
    for (uint64_t i = 0u; i < enc->n; i++) {
        semqi_unpack_row(&codec->config, enc->rows + (size_t)i * codec->bpv,
                         out + (size_t)i * codec->units);
    }
    semqi_ok(err);
    return SEMQ_OK;
}