neuronal / engine /src /memory.c
amogaddy's picture
amogaddyofficial
claude sonnet 4.6
Neuronal
d601f7c
Raw History Blame Contribute Delete
21.6 kB
#include "memory.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <direct.h>
#define make_dir(p) _mkdir(p)
#else
#include <sys/stat.h>
#define make_dir(p) mkdir(p, 0755)
#endif
static uint32_t crc32(const uint8_t *p, size_t n) {
uint32_t c = 0xFFFFFFFFu;
for (size_t i = 0; i < n; i++) {
c ^= p[i];
for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1)));
}
return ~c;
}
static void layer_file(const MemoryStore *m, int layer, char *out, size_t cap) {
snprintf(out, cap, "%s/strato_%02d.bin", m->dir, layer);
}
// ---- mappa (navigatore) ---------------------------------------------------------------------
static void save_map(const MemoryStore *m) {
char path[1100], tmp[1110];
snprintf(path, sizeof path, "%s/mappa.txt", m->dir);
snprintf(tmp, sizeof tmp, "%s.tmp", path);
FILE *f = fopen(tmp, "wb");
if (!f) return;
fprintf(f, "# Mappa della memoria di Neuronal: id\tbyte\tcrc\tdata\ttitolo\tgruppi\timpronta\n");
for (int i = 0; i < m->n_mems; i++) {
const Memory *x = &m->mems[i];
fprintf(f, "%d\t%d\t%08x\t%lld\t%s\t", x->id, x->len, x->crc, (long long)x->t, x->title);
for (int k = 0; k < x->n_groups; k++) fprintf(f, "%s%d", k ? "," : "", x->groups[k]);
fputc('\t', f);
for (int k = 0; k < x->n_key; k++) fprintf(f, "%s%d:%.3f", k ? "," : "", x->key_g[k], x->key_w[k]);
fputc('\n', f);
}
fclose(f);
remove(path);
rename(tmp, path);
}
static void emb_path(const MemoryStore *m, int id, char *out, size_t cap) { snprintf(out, cap, "%s/impronte/%d.bin", m->dir, id); }
static void save_emb(const MemoryStore *m, const Memory *x) {
char dir[1100], path[1200];
snprintf(dir, sizeof dir, "%s/impronte", m->dir);
make_dir(dir);
emb_path(m, x->id, path, sizeof path);
FILE *f = fopen(path, "wb");
if (f) { fwrite(x->emb, sizeof(float), (size_t)m->emb_dim, f); fclose(f); }
}
static float *load_emb(const MemoryStore *m, int id) {
char path[1200];
emb_path(m, id, path, sizeof path);
FILE *f = fopen(path, "rb");
if (!f) return NULL;
float *e = malloc(sizeof(float) * (size_t)m->emb_dim);
bool ok = fread(e, sizeof(float), (size_t)m->emb_dim, f) == (size_t)m->emb_dim;
fclose(f);
if (!ok) { free(e); return NULL; }
return e;
}
static void load_map(MemoryStore *m) {
char path[1100];
snprintf(path, sizeof path, "%s/mappa.txt", m->dir);
FILE *f = fopen(path, "rb");
if (!f) return;
static char line[1 << 17];
while (fgets(line, sizeof line, f)) {
if (line[0] == '#') continue;
line[strcspn(line, "\r\n")] = 0;
char *fld[7] = {0};
int n = 0;
for (char *p = line; n < 7;) {
fld[n++] = p;
char *t = strchr(p, '\t');
if (!t) break;
*t = 0;
p = t + 1;
}
if (n < 7) continue;
if (m->n_mems == m->cap_mems) {
m->cap_mems = m->cap_mems ? m->cap_mems * 2 : 16;
m->mems = realloc(m->mems, sizeof(Memory) * (size_t)m->cap_mems);
}
Memory *x = &m->mems[m->n_mems];
memset(x, 0, sizeof *x);
x->id = atoi(fld[0]);
x->len = atoi(fld[1]);
x->crc = (uint32_t)strtoul(fld[2], NULL, 16);
x->t = (time_t)atoll(fld[3]);
snprintf(x->title, sizeof x->title, "%s", fld[4]);
int cap = 16;
x->groups = malloc(sizeof(int) * (size_t)cap);
for (char *p = fld[5]; *p;) {
if (x->n_groups == cap) { cap *= 2; x->groups = realloc(x->groups, sizeof(int) * (size_t)cap); }
x->groups[x->n_groups++] = (int)strtol(p, &p, 10);
if (*p == ',') p++;
}
for (char *p = fld[6]; *p && x->n_key < MEM_KEY;) {
x->key_g[x->n_key] = (int)strtol(p, &p, 10);
if (*p == ':') p++;
x->key_w[x->n_key++] = strtof(p, &p);
if (*p == ',') p++;
}
bool ok = true;
for (int k = 0; k < x->n_groups; k++) ok &= x->groups[k] >= 0 && x->groups[k] < m->n_groups;
if (!ok) { free(x->groups); continue; }
for (int k = 0; k < x->n_groups; k++) m->owner[x->groups[k]] = x->id;
if (x->id >= m->next_id) m->next_id = x->id + 1;
x->emb = load_emb(m, x->id);
m->n_mems++;
}
fclose(f);
}
bool mem_open(MemoryStore *m, const char *dir, int n_layer, int n_ff, int n_embd) {
memset(m, 0, sizeof *m);
snprintf(m->dir, sizeof m->dir, "%s", dir);
m->n_layer = n_layer;
m->n_ff = n_ff;
m->per_layer = (n_ff + MEM_GROUP - 1) / MEM_GROUP;
m->n_groups = m->per_layer * n_layer;
m->owner = calloc((size_t)m->n_groups, sizeof(int));
m->next_id = 1;
m->n_embd = n_embd;
m->emb_dim = MEM_EMB_LAYERS * n_embd;
static const float at[MEM_EMB_LAYERS] = {0.625f}; // strato 40 di 64: il migliore nelle prove del navigatore
for (int k = 0; k < MEM_EMB_LAYERS; k++) m->emb_layer[k] = (int)(at[k] * n_layer);
make_dir(dir);
// un file per strato, 16 byte per gruppo: creato vuoto la prima volta
size_t bytes = (size_t)m->per_layer * MEM_SLOT;
uint8_t *zero = calloc(bytes, 1);
for (int l = 0; l < n_layer; l++) {
char path[1100];
layer_file(m, l, path, sizeof path);
FILE *f = fopen(path, "rb");
long size = -1;
if (f) { fseek(f, 0, SEEK_END); size = ftell(f); fclose(f); }
if (size != (long)bytes) {
f = fopen(path, size < 0 ? "wb" : "r+b");
if (!f) f = fopen(path, "wb");
if (!f) { free(zero); return false; }
if (size < 0 || size < (long)bytes) { fseek(f, size < 0 ? 0 : size, SEEK_SET); fwrite(zero, 1, bytes - (size_t)(size < 0 ? 0 : size), f); }
fclose(f);
}
}
free(zero);
load_map(m);
return true;
}
void mem_set_baseline(MemoryStore *m, const Atlas *a) {
free(m->mu); free(m->bg_mu); free(m->bg_sd);
m->bg_mu = m->bg_sd = NULL;
if (a->group_act && a->n_groups == m->n_groups && a->n_act > 1) { // comportamento normale di ogni gruppo
m->bg_mu = calloc((size_t)m->n_groups, sizeof(float));
m->bg_sd = calloc((size_t)m->n_groups, sizeof(float));
for (int g = 0; g < m->n_groups; g++) {
double s = 0, s2 = 0;
for (int p = 0; p < a->n_act; p++) { double v = a->group_act[(size_t)p * a->n_groups + g]; s += v; s2 += v * v; }
double mu = s / a->n_act, var = s2 / a->n_act - mu * mu;
m->bg_mu[g] = (float)mu;
m->bg_sd[g] = (float)fmax(sqrt(var > 0 ? var : 0), 0.05 * mu + 1e-6);
}
}
m->mu = calloc((size_t)m->n_layer, sizeof(float));
for (int l = 0; l < m->n_layer && l < a->n_layer; l++) {
double s = 0;
for (int p = 0; p < a->n_act; p++) s += a->layer_act[(size_t)p * a->n_layer + l];
m->mu[l] = a->n_act ? (float)(s / a->n_act) : 0;
}
}
void mem_close(MemoryStore *m) {
for (int i = 0; i < m->n_mems; i++) { free(m->mems[i].groups); free(m->mems[i].emb); }
free(m->mems); free(m->owner); free(m->mu); free(m->bg_mu); free(m->bg_sd); free(m->emb_mu); free(m->emb_sd);
memset(m, 0, sizeof *m);
}
Memory *mem_find(MemoryStore *m, int id) {
for (int i = 0; i < m->n_mems; i++) if (m->mems[i].id == id) return &m->mems[i];
return NULL;
}
long mem_used_groups(const MemoryStore *m) {
long n = 0;
for (int g = 0; g < m->n_groups; g++) n += m->owner[g] != 0;
return n;
}
// ---- attivazione dei gruppi ------------------------------------------------------------------
// Il cervello legge il testo dentro una formula che gli fa condensare il significato sull'ultima parola:
// l'attività dei gruppi in quel punto è l'impronta del testo.
static void normalize_emb(const MemoryStore *m, float *e) {
double ss = 0;
for (int i = 0; i < m->emb_dim; i++) {
if (m->emb_mu) e[i] -= m->emb_mu[i];
ss += (double)e[i] * e[i];
}
float inv = ss > 0 ? (float)(1.0 / sqrt(ss)) : 0;
for (int i = 0; i < m->emb_dim; i++) e[i] *= inv;
}
static bool read_text(MemoryStore *m, Brain *b, const char *text, float *vec, float *emb_raw) {
BrainInfo info = brain_info(b);
size_t tl = strlen(text);
if (tl > 1500) { // un messaggio lungo: basta l'inizio per capire di cosa parla
tl = 1500;
while (tl > 0 && ((unsigned char)text[tl] & 0xC0) == 0x80) tl--;
}
char *prompt = malloc(tl + 96);
sprintf(prompt, "Frase: \"%.*s\"\nIn una parola, questa frase parla di:", (int)tl, text);
if (emb_raw) brain_capture_begin(b);
int n = brain_feed(b, prompt);
if (emb_raw) { // stato interno medio su tutte le parole lette
long nt;
const double *sum = brain_capture_x(b, &nt);
for (int k = 0; k < MEM_EMB_LAYERS; k++)
for (int d = 0; d < m->n_embd; d++)
emb_raw[(size_t)k * m->n_embd + d] = nt > 0 ? (float)(sum[(size_t)m->emb_layer[k] * m->n_embd + d] / nt) : 0;
brain_capture_end(b);
}
free(prompt);
if (n <= 0) return false;
if (!vec) return true;
const float *act = brain_last_activation(b);
for (int l = 0; l < m->n_layer; l++)
for (int q = 0; q < m->per_layer; q++) {
double s = 0;
int cnt = 0;
for (int j = q * MEM_GROUP; j < (q + 1) * MEM_GROUP && j < info.n_ff; j++, cnt++) s += fabsf(act[(size_t)l * info.n_ff + j]);
vec[l * m->per_layer + q] = (float)(s / cnt);
}
return true;
}
bool mem_activation(MemoryStore *m, Brain *b, const char *text, float *vec, float *emb) {
if (!read_text(m, b, text, vec, emb)) return false;
if (emb) normalize_emb(m, emb);
return true;
}
void mem_set_background(MemoryStore *m, Brain *b, const Atlas *a) {
int total = 0;
for (int c = 0; c < a->n_cat; c++) total += a->cat[c].n_prompts;
if (total < 2) return;
float *all = malloc(sizeof(float) * (size_t)total * m->n_groups);
float *eall = malloc(sizeof(float) * (size_t)total * m->emb_dim);
int n = 0;
for (int c = 0; c < a->n_cat; c++)
for (int p = 0; p < a->cat[c].n_prompts; p++)
if (read_text(m, b, a->cat[c].prompts[p], all + (size_t)n * m->n_groups, eall + (size_t)n * m->emb_dim)) n++;
if (n < 2) { free(all); free(eall); return; }
free(m->emb_mu); free(m->emb_sd);
m->emb_mu = calloc((size_t)m->emb_dim, sizeof(float));
m->emb_sd = calloc((size_t)m->emb_dim, sizeof(float));
for (int d = 0; d < m->emb_dim; d++) {
double s = 0, s2 = 0;
for (int i = 0; i < n; i++) { double v = eall[(size_t)i * m->emb_dim + d]; s += v; s2 += v * v; }
double mu = s / n, var = s2 / n - mu * mu;
m->emb_mu[d] = (float)mu;
m->emb_sd[d] = (float)fmax(sqrt(var > 0 ? var : 0), 1e-3 + 0.05 * fabs(mu));
}
free(eall);
free(m->bg_mu); free(m->bg_sd);
m->bg_mu = calloc((size_t)m->n_groups, sizeof(float));
m->bg_sd = calloc((size_t)m->n_groups, sizeof(float));
for (int g = 0; g < m->n_groups; g++) {
double s = 0, s2 = 0;
for (int i = 0; i < n; i++) { double v = all[(size_t)i * m->n_groups + g]; s += v; s2 += v * v; }
double mu = s / n, var = s2 / n - mu * mu;
m->bg_mu[g] = (float)mu;
m->bg_sd[g] = (float)fmax(sqrt(var > 0 ? var : 0), 0.05 * mu + 1e-6);
}
free(all);
}
#define FONDO_MAGIC "NEURONAL-FONDO-1"
bool mem_background_save(const MemoryStore *m, const char *path, const char *key) {
if (!m->emb_mu || !m->emb_sd || !m->bg_mu || !m->bg_sd) return false;
FILE *f = fopen(path, "wb");
if (!f) return false;
unsigned kl = (unsigned)strlen(key);
int dims[2] = {m->n_groups, m->emb_dim};
bool ok = fwrite(FONDO_MAGIC, 1, sizeof FONDO_MAGIC, f) == sizeof FONDO_MAGIC && fwrite(&kl, 4, 1, f) == 1 &&
fwrite(key, 1, kl, f) == kl && fwrite(dims, sizeof dims, 1, f) == 1 &&
fwrite(m->emb_mu, sizeof(float), (size_t)m->emb_dim, f) == (size_t)m->emb_dim &&
fwrite(m->emb_sd, sizeof(float), (size_t)m->emb_dim, f) == (size_t)m->emb_dim &&
fwrite(m->bg_mu, sizeof(float), (size_t)m->n_groups, f) == (size_t)m->n_groups &&
fwrite(m->bg_sd, sizeof(float), (size_t)m->n_groups, f) == (size_t)m->n_groups;
ok = fclose(f) == 0 && ok;
if (!ok) remove(path);
return ok;
}
bool mem_background_load(MemoryStore *m, const char *path, const char *key) {
FILE *f = fopen(path, "rb");
if (!f) return false;
char magic[sizeof FONDO_MAGIC], k[1024];
unsigned kl = 0;
int dims[2] = {0, 0};
bool ok = fread(magic, 1, sizeof magic, f) == sizeof magic && !memcmp(magic, FONDO_MAGIC, sizeof magic) &&
fread(&kl, 4, 1, f) == 1 && kl < sizeof k && fread(k, 1, kl, f) == kl;
if (ok) { k[kl] = 0; ok = !strcmp(k, key); }
ok = ok && fread(dims, sizeof dims, 1, f) == 1 && dims[0] == m->n_groups && dims[1] == m->emb_dim;
if (ok) {
float *emu = malloc(sizeof(float) * (size_t)m->emb_dim), *esd = malloc(sizeof(float) * (size_t)m->emb_dim);
float *bmu = malloc(sizeof(float) * (size_t)m->n_groups), *bsd = malloc(sizeof(float) * (size_t)m->n_groups);
ok = fread(emu, sizeof(float), (size_t)m->emb_dim, f) == (size_t)m->emb_dim &&
fread(esd, sizeof(float), (size_t)m->emb_dim, f) == (size_t)m->emb_dim &&
fread(bmu, sizeof(float), (size_t)m->n_groups, f) == (size_t)m->n_groups &&
fread(bsd, sizeof(float), (size_t)m->n_groups, f) == (size_t)m->n_groups;
if (ok) {
free(m->emb_mu); free(m->emb_sd); free(m->bg_mu); free(m->bg_sd);
m->emb_mu = emu; m->emb_sd = esd; m->bg_mu = bmu; m->bg_sd = bsd;
} else {
free(emu); free(esd); free(bmu); free(bsd);
}
}
fclose(f);
return ok;
}
// Profilo normalizzato: quanto ogni gruppo si accende più (o meno) del suo solito (z-score),
// poi centrato e portato a lunghezza 1. Senza riferimento, solo centrato.
static const MemoryStore *CUR;
static void center(float *v, int n) {
if (CUR && CUR->bg_mu && n == CUR->n_groups)
for (int i = 0; i < n; i++) v[i] = (v[i] - CUR->bg_mu[i]) / CUR->bg_sd[i];
double mean = 0, ss = 0;
for (int i = 0; i < n; i++) mean += v[i];
mean /= n;
for (int i = 0; i < n; i++) { v[i] -= (float)mean; ss += (double)v[i] * v[i]; }
float inv = ss > 0 ? (float)(1.0 / sqrt(ss)) : 0;
for (int i = 0; i < n; i++) v[i] *= inv;
}
// ---- scrittura e lettura dei bit ------------------------------------------------------------------
static bool slot_io(MemoryStore *m, int g, uint8_t *buf, bool write) {
int layer = g / m->per_layer, q = g % m->per_layer;
char path[1100];
layer_file(m, layer, path, sizeof path);
FILE *f = fopen(path, write ? "r+b" : "rb");
if (!f) return false;
bool ok = fseek(f, (long)q * MEM_SLOT, SEEK_SET) == 0 &&
(write ? fwrite(buf, 1, MEM_SLOT, f) : fread(buf, 1, MEM_SLOT, f)) == MEM_SLOT;
fclose(f);
return ok;
}
int mem_add(MemoryStore *m, Brain *b, const char *title, const char *text, char *err, size_t cap) {
int len = (int)strlen(text);
if (len == 0 || len > MEM_MAX_TEXT) { snprintf(err, cap, "il testo deve avere da 1 a %d caratteri", MEM_MAX_TEXT); return -1; }
int need = (len + MEM_SLOT - 1) / MEM_SLOT;
long free_groups = m->n_groups - mem_used_groups(m);
if (need > free_groups) { snprintf(err, cap, "memoria piena: servono %d gruppi, liberi %ld", need, free_groups); return -1; }
float *vec = malloc(sizeof(float) * (size_t)m->n_groups);
float *emb = malloc(sizeof(float) * (size_t)m->emb_dim);
if (!mem_activation(m, b, text, vec, emb)) { free(vec); free(emb); snprintf(err, cap, "lettura del testo fallita"); return -1; }
if (m->n_mems == m->cap_mems) {
m->cap_mems = m->cap_mems ? m->cap_mems * 2 : 16;
m->mems = realloc(m->mems, sizeof(Memory) * (size_t)m->cap_mems);
}
Memory *x = &m->mems[m->n_mems];
memset(x, 0, sizeof *x);
x->id = m->next_id++;
snprintf(x->title, sizeof x->title, "%s", title && *title ? title : "Ricordo");
for (char *c = x->title; *c; c++) if (*c == '\t' || *c == '\n' || *c == '\r') *c = ' ';
x->len = len;
x->crc = crc32((const uint8_t *)text, (size_t)len);
x->t = time(NULL);
x->emb = emb;
// impronta: i gruppi che si accendono di più rispetto al normale
float *cen = malloc(sizeof(float) * (size_t)m->n_groups);
memcpy(cen, vec, sizeof(float) * (size_t)m->n_groups);
CUR = m;
center(cen, m->n_groups);
int *order = malloc(sizeof(int) * (size_t)m->n_groups);
for (int g = 0; g < m->n_groups; g++) order[g] = g;
// ordinamento per attivazione decrescente (selezione parziale sufficiente: servono pochi gruppi)
int want = need + MEM_KEY + 256;
if (want > m->n_groups) want = m->n_groups;
for (int i = 0; i < want; i++) {
int best = i;
for (int k = i + 1; k < m->n_groups; k++) if (cen[order[k]] > cen[order[best]]) best = k;
int t = order[i]; order[i] = order[best]; order[best] = t;
}
for (int k = 0; k < MEM_KEY && k < m->n_groups; k++) { x->key_g[k] = order[k]; x->key_w[k] = cen[order[k]]; x->n_key++; }
// casa dei byte: i gruppi liberi più accesi (poi, se servono, qualsiasi gruppo libero)
x->groups = malloc(sizeof(int) * (size_t)need);
for (int i = 0; i < m->n_groups && x->n_groups < need; i++) {
int g = i < want ? order[i] : -1;
if (g < 0) break;
if (!m->owner[g]) x->groups[x->n_groups++] = g;
}
for (int g = 0; g < m->n_groups && x->n_groups < need; g++) {
bool used = m->owner[g] != 0;
for (int k = 0; k < x->n_groups && !used; k++) used = x->groups[k] == g;
if (!used) x->groups[x->n_groups++] = g;
}
bool ok = true;
for (int k = 0; k < x->n_groups && ok; k++) {
uint8_t slot[MEM_SLOT] = {0};
int from = k * MEM_SLOT, n = len - from < MEM_SLOT ? len - from : MEM_SLOT;
memcpy(slot, text + from, (size_t)n);
ok = slot_io(m, x->groups[k], slot, true);
}
free(vec); free(cen); free(order);
if (!ok) { free(x->groups); free(x->emb); snprintf(err, cap, "scrittura sul disco fallita"); return -1; }
for (int k = 0; k < x->n_groups; k++) m->owner[x->groups[k]] = x->id;
m->n_mems++;
save_emb(m, x);
save_map(m);
return x->id;
}
bool mem_delete(MemoryStore *m, int id) {
for (int i = 0; i < m->n_mems; i++) {
Memory *x = &m->mems[i];
if (x->id != id) continue;
uint8_t zero[MEM_SLOT] = {0};
for (int k = 0; k < x->n_groups; k++) { slot_io(m, x->groups[k], zero, true); m->owner[x->groups[k]] = 0; }
char path[1200];
emb_path(m, x->id, path, sizeof path);
remove(path);
free(x->groups);
free(x->emb);
m->mems[i] = m->mems[--m->n_mems];
save_map(m);
return true;
}
return false;
}
char *mem_read(MemoryStore *m, const Memory *x) {
char *out = malloc((size_t)x->n_groups * MEM_SLOT + 1);
for (int k = 0; k < x->n_groups; k++)
if (!slot_io(m, x->groups[k], (uint8_t *)out + (size_t)k * MEM_SLOT, false)) { free(out); return NULL; }
out[x->len] = 0;
if (crc32((const uint8_t *)out, (size_t)x->len) != x->crc) { free(out); return NULL; } // bit danneggiati
return out;
}
int mem_recall(MemoryStore *m, const float *qemb, int *ids, float *scores, int max) {
int n = 0;
for (int i = 0; i < m->n_mems; i++) {
const Memory *x = &m->mems[i];
if (!x->emb) continue;
double dot = 0;
for (int d = 0; d < m->emb_dim; d++) dot += (double)x->emb[d] * qemb[d];
float sc = (float)dot; // coseno tra le due impronte
int pos = n < max ? n++ : max - 1;
if (pos == max - 1 && n == max && sc <= scores[pos]) continue;
while (pos > 0 && scores[pos - 1] < sc) { scores[pos] = scores[pos - 1]; ids[pos] = ids[pos - 1]; pos--; }
scores[pos] = sc;
ids[pos] = x->id;
}
return n;
}
void mem_json(MemoryStore *m, SBuf *o) {
long used = mem_used_groups(m);
sb_printf(o, "{\"groups\":%d,\"per_layer\":%d,\"group_neurons\":%d,\"slot_bits\":%d,\"used\":%ld,\"capacity_bytes\":%ld,\"memories\":[",
m->n_groups, m->per_layer, MEM_GROUP, MEM_SLOT * 8, used, (long)m->n_groups * MEM_SLOT);
for (int i = 0; i < m->n_mems; i++) {
const Memory *x = &m->mems[i];
char *text = mem_read(m, x);
sb_printf(o, "%s{\"id\":%d,\"bytes\":%d,\"t\":%lld,\"intact\":%s,\"title\":", i ? "," : "", x->id, x->len, (long long)x->t, text ? "true" : "false");
sb_json_str(o, x->title);
sb_puts(o, ",\"text\":");
sb_json_str(o, text ? text : "");
sb_puts(o, ",\"groups\":[");
for (int k = 0; k < x->n_groups; k++) sb_printf(o, "%s%d", k ? "," : "", x->groups[k]);
sb_puts(o, "]}");
free(text);
}
sb_puts(o, "]}");
}