Coverage Report

Created: 2026-02-23 20:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/Users/alexjokela/projects/lattice/src/toml_ops.c
Line
Count
Source
1
#include "toml_ops.h"
2
#include <stdlib.h>
3
#include <string.h>
4
#include <stdio.h>
5
#include <stdarg.h>
6
#include <ctype.h>
7
8
/* ========================================================================
9
 * Internal: TOML Parser
10
 * ======================================================================== */
11
12
typedef struct {
13
    const char *src;
14
    size_t      pos;
15
    char       *err;
16
} TomlParser;
17
18
90
static void tp_skip_ws(TomlParser *p) {
19
144
    while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t')
20
54
        p->pos++;
21
90
}
22
23
66
static void tp_skip_ws_and_newlines(TomlParser *p) {
24
72
    while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t' ||
25
72
           p->src[p->pos] == '\n' || p->src[p->pos] == '\r')
26
6
        p->pos++;
27
66
}
28
29
78
static void tp_skip_comment(TomlParser *p) {
30
78
    if (p->src[p->pos] == '#') {
31
0
        while (p->src[p->pos] && p->src[p->pos] != '\n')
32
0
            p->pos++;
33
0
    }
34
78
}
35
36
30
static void tp_skip_line_rest(TomlParser *p) {
37
30
    tp_skip_ws(p);
38
30
    tp_skip_comment(p);
39
30
    if (p->src[p->pos] == '\n') p->pos++;
40
12
    else if (p->src[p->pos] == '\r') {
41
0
        p->pos++;
42
0
        if (p->src[p->pos] == '\n') p->pos++;
43
0
    }
44
30
}
45
46
0
static void tp_error(TomlParser *p, const char *msg) {
47
0
    if (!p->err) {
48
0
        size_t len = strlen(msg) + 64;
49
0
        p->err = malloc(len);
50
0
        snprintf(p->err, len, "toml_parse error at position %zu: %s", p->pos, msg);
51
0
    }
52
0
}
53
54
/* Forward declarations */
55
static LatValue tp_parse_value(TomlParser *p);
56
57
/* ── Parse bare key ── */
58
30
static char *tp_parse_bare_key(TomlParser *p) {
59
30
    size_t start = p->pos;
60
171
    while (isalnum((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_' ||
61
171
           p->src[p->pos] == '-')
62
141
        p->pos++;
63
30
    if (p->pos == start) {
64
0
        tp_error(p, "expected key");
65
0
        return NULL;
66
0
    }
67
30
    return strndup(p->src + start, p->pos - start);
68
30
}
69
70
/* ── Parse quoted string ── */
71
9
static char *tp_parse_quoted_string(TomlParser *p) {
72
9
    char quote = p->src[p->pos];
73
9
    p->pos++;
74
75
9
    size_t cap = 64;
76
9
    char *buf = malloc(cap);
77
9
    size_t len = 0;
78
79
9
    if (quote == '"') {
80
63
        while (p->src[p->pos] && p->src[p->pos] != '"') {
81
54
            if (p->src[p->pos] == '\\') {
82
0
                p->pos++;
83
0
                char c = p->src[p->pos++];
84
0
                char esc;
85
0
                switch (c) {
86
0
                    case 'n': esc = '\n'; break;
87
0
                    case 't': esc = '\t'; break;
88
0
                    case 'r': esc = '\r'; break;
89
0
                    case '\\': esc = '\\'; break;
90
0
                    case '"': esc = '"'; break;
91
0
                    default:
92
0
                        tp_error(p, "invalid escape sequence");
93
0
                        free(buf);
94
0
                        return NULL;
95
0
                }
96
0
                if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); }
97
0
                buf[len++] = esc;
98
54
            } else {
99
54
                if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); }
100
54
                buf[len++] = p->src[p->pos++];
101
54
            }
102
54
        }
103
9
    } else {
104
0
        while (p->src[p->pos] && p->src[p->pos] != '\'') {
105
0
            if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); }
106
0
            buf[len++] = p->src[p->pos++];
107
0
        }
108
0
    }
109
110
9
    if (p->src[p->pos] != quote) {
111
0
        tp_error(p, "unterminated string");
112
0
        free(buf);
113
0
        return NULL;
114
0
    }
115
9
    p->pos++;
116
9
    buf[len] = '\0';
117
9
    return buf;
118
9
}
119
120
/* ── Parse a key (bare or quoted) ── */
121
30
static char *tp_parse_key(TomlParser *p) {
122
30
    if (p->src[p->pos] == '"' || p->src[p->pos] == '\'')
123
0
        return tp_parse_quoted_string(p);
124
30
    return tp_parse_bare_key(p);
125
30
}
126
127
/* ── Parse string value ── */
128
9
static LatValue tp_parse_string_value(TomlParser *p) {
129
9
    char *s = tp_parse_quoted_string(p);
130
9
    if (!s) return value_unit();
131
9
    LatValue v = value_string(s);
132
9
    free(s);
133
9
    return v;
134
9
}
135
136
/* ── Parse number ── */
137
18
static LatValue tp_parse_number(TomlParser *p) {
138
18
    size_t start = p->pos;
139
18
    if (p->src[p->pos] == '-' || p->src[p->pos] == '+')
140
0
        p->pos++;
141
18
    bool is_float = false;
142
143
51
    while (isdigit((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_')
144
33
        p->pos++;
145
18
    if (p->src[p->pos] == '.') {
146
0
        is_float = true;
147
0
        p->pos++;
148
0
        while (isdigit((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_')
149
0
            p->pos++;
150
0
    }
151
18
    if (p->src[p->pos] == 'e' || p->src[p->pos] == 'E') {
152
0
        is_float = true;
153
0
        p->pos++;
154
0
        if (p->src[p->pos] == '+' || p->src[p->pos] == '-') p->pos++;
155
0
        while (isdigit((unsigned char)p->src[p->pos])) p->pos++;
156
0
    }
157
158
18
    size_t nlen = p->pos - start;
159
18
    char *numstr = malloc(nlen + 1);
160
18
    size_t j = 0;
161
51
    for (size_t i = start; i < p->pos; i++) {
162
33
        if (p->src[i] != '_') numstr[j++] = p->src[i];
163
33
    }
164
18
    numstr[j] = '\0';
165
166
18
    LatValue result;
167
18
    if (is_float) {
168
0
        result = value_float(strtod(numstr, NULL));
169
18
    } else {
170
18
        result = value_int(strtoll(numstr, NULL, 10));
171
18
    }
172
18
    free(numstr);
173
18
    return result;
174
18
}
175
176
/* ── Array push helper ── */
177
9
static void arr_push(LatValue **elems, size_t *len, size_t *cap, LatValue v) {
178
9
    if (*len >= *cap) {
179
0
        *cap = *cap < 4 ? 4 : *cap * 2;
180
0
        *elems = realloc(*elems, *cap * sizeof(LatValue));
181
0
    }
182
9
    (*elems)[(*len)++] = v;
183
9
}
184
185
/* ── Parse inline array ── */
186
3
static LatValue tp_parse_array(TomlParser *p) {
187
3
    p->pos++; /* skip [ */
188
3
    size_t cap = 4, len = 0;
189
3
    LatValue *elems = malloc(cap * sizeof(LatValue));
190
191
3
    tp_skip_ws_and_newlines(p);
192
3
    tp_skip_comment(p);
193
3
    tp_skip_ws_and_newlines(p);
194
195
12
    while (p->src[p->pos] != ']' && p->src[p->pos] != '\0') {
196
9
        if (p->err) {
197
0
            for (size_t i = 0; i < len; i++) value_free(&elems[i]);
198
0
            free(elems);
199
0
            return value_unit();
200
0
        }
201
202
9
        LatValue elem = tp_parse_value(p);
203
9
        if (p->err) {
204
0
            value_free(&elem);
205
0
            for (size_t i = 0; i < len; i++) value_free(&elems[i]);
206
0
            free(elems);
207
0
            return value_unit();
208
0
        }
209
210
9
        arr_push(&elems, &len, &cap, elem);
211
212
9
        tp_skip_ws_and_newlines(p);
213
9
        tp_skip_comment(p);
214
9
        tp_skip_ws_and_newlines(p);
215
9
        if (p->src[p->pos] == ',') {
216
6
            p->pos++;
217
6
            tp_skip_ws_and_newlines(p);
218
6
            tp_skip_comment(p);
219
6
            tp_skip_ws_and_newlines(p);
220
6
        }
221
9
    }
222
223
3
    if (p->src[p->pos] != ']') {
224
0
        tp_error(p, "unterminated array");
225
0
        for (size_t i = 0; i < len; i++) value_free(&elems[i]);
226
0
        free(elems);
227
0
        return value_unit();
228
0
    }
229
3
    p->pos++;
230
3
    LatValue arr = value_array(elems, len);
231
3
    free(elems);
232
3
    return arr;
233
3
}
234
235
/* ── Parse inline table ── */
236
0
static LatValue tp_parse_inline_table(TomlParser *p) {
237
0
    p->pos++; /* skip { */
238
0
    LatValue map = value_map_new();
239
240
0
    tp_skip_ws(p);
241
0
    while (p->src[p->pos] != '}' && p->src[p->pos] != '\0') {
242
0
        if (p->err) { value_free(&map); return value_unit(); }
243
244
0
        char *key = tp_parse_key(p);
245
0
        if (!key || p->err) { free(key); value_free(&map); return value_unit(); }
246
247
0
        tp_skip_ws(p);
248
0
        if (p->src[p->pos] != '=') {
249
0
            tp_error(p, "expected '=' after key");
250
0
            free(key); value_free(&map); return value_unit();
251
0
        }
252
0
        p->pos++;
253
0
        tp_skip_ws(p);
254
255
0
        LatValue val = tp_parse_value(p);
256
0
        if (p->err) { free(key); value_free(&val); value_free(&map); return value_unit(); }
257
258
0
        lat_map_set(map.as.map.map, key, &val);
259
0
        free(key);
260
261
0
        tp_skip_ws(p);
262
0
        if (p->src[p->pos] == ',') {
263
0
            p->pos++;
264
0
            tp_skip_ws(p);
265
0
        }
266
0
    }
267
268
0
    if (p->src[p->pos] != '}') {
269
0
        tp_error(p, "unterminated inline table");
270
0
        value_free(&map);
271
0
        return value_unit();
272
0
    }
273
0
    p->pos++;
274
0
    return map;
275
0
}
276
277
/* ── Parse a value ── */
278
36
static LatValue tp_parse_value(TomlParser *p) {
279
36
    char c = p->src[p->pos];
280
36
    if (c == '"' || c == '\'') return tp_parse_string_value(p);
281
27
    if (c == '[') return tp_parse_array(p);
282
24
    if (c == '{') return tp_parse_inline_table(p);
283
24
    if (c == 't' && strncmp(p->src + p->pos, "true", 4) == 0 &&
284
24
        !isalnum((unsigned char)p->src[p->pos + 4])) {
285
3
        p->pos += 4;
286
3
        return value_bool(true);
287
3
    }
288
21
    if (c == 'f' && strncmp(p->src + p->pos, "false", 5) == 0 &&
289
21
        !isalnum((unsigned char)p->src[p->pos + 5])) {
290
3
        p->pos += 5;
291
3
        return value_bool(false);
292
3
    }
293
18
    if (isdigit((unsigned char)c) || c == '-' || c == '+')
294
18
        return tp_parse_number(p);
295
296
0
    tp_error(p, "unexpected character");
297
0
    return value_unit();
298
18
}
299
300
/* ── Get or create nested map from dotted key path ── */
301
3
static LatMap *tp_ensure_table(LatValue *root, char **parts, size_t count) {
302
3
    LatMap *cur = root->as.map.map;
303
6
    for (size_t i = 0; i < count; i++) {
304
3
        LatValue *existing = (LatValue *)lat_map_get(cur, parts[i]);
305
3
        if (existing && existing->type == VAL_MAP) {
306
0
            cur = existing->as.map.map;
307
3
        } else {
308
3
            LatValue sub = value_map_new();
309
3
            lat_map_set(cur, parts[i], &sub);
310
3
            LatValue *inserted = (LatValue *)lat_map_get(cur, parts[i]);
311
3
            cur = inserted->as.map.map;
312
3
        }
313
3
    }
314
3
    return cur;
315
3
}
316
317
/* ── Parse dotted key into parts ── */
318
30
static size_t tp_parse_dotted_key(TomlParser *p, char **parts, size_t max_parts) {
319
30
    size_t count = 0;
320
30
    parts[count] = tp_parse_key(p);
321
30
    if (!parts[count] || p->err) return 0;
322
30
    count++;
323
324
30
    while (p->src[p->pos] == '.' && count < max_parts) {
325
0
        p->pos++;
326
0
        parts[count] = tp_parse_key(p);
327
0
        if (!parts[count] || p->err) {
328
0
            for (size_t i = 0; i < count; i++) free(parts[i]);
329
0
            return 0;
330
0
        }
331
0
        count++;
332
0
    }
333
30
    return count;
334
30
}
335
336
/* ── Main TOML parse function ── */
337
15
LatValue toml_ops_parse(const char *toml_str, char **err) {
338
15
    TomlParser p = { .src = toml_str, .pos = 0, .err = NULL };
339
15
    LatValue root = value_map_new();
340
15
    LatMap *current_table = root.as.map.map;
341
342
45
    while (p.src[p.pos] != '\0') {
343
30
        tp_skip_ws_and_newlines(&p);
344
30
        tp_skip_comment(&p);
345
30
        if (p.src[p.pos] == '\0') break;
346
30
        if (p.src[p.pos] == '\n' || p.src[p.pos] == '\r') { p.pos++; continue; }
347
348
30
        if (p.err) break;
349
350
        /* Table header: [key] or [[key]] */
351
30
        if (p.src[p.pos] == '[') {
352
3
            bool is_array_table = false;
353
3
            p.pos++;
354
3
            if (p.src[p.pos] == '[') {
355
0
                is_array_table = true;
356
0
                p.pos++;
357
0
            }
358
3
            tp_skip_ws(&p);
359
360
3
            char *parts[32];
361
3
            size_t count = tp_parse_dotted_key(&p, parts, 32);
362
3
            if (count == 0 || p.err) {
363
0
                value_free(&root);
364
0
                *err = p.err ? p.err : strdup("toml_parse: invalid table header");
365
0
                return value_unit();
366
0
            }
367
368
3
            tp_skip_ws(&p);
369
370
3
            if (is_array_table) {
371
0
                if (p.src[p.pos] != ']' || p.src[p.pos + 1] != ']') {
372
0
                    for (size_t i = 0; i < count; i++) free(parts[i]);
373
0
                    tp_error(&p, "expected ']]'");
374
0
                    break;
375
0
                }
376
0
                p.pos += 2;
377
378
                /* Navigate to parent */
379
0
                LatMap *parent = root.as.map.map;
380
0
                for (size_t i = 0; i + 1 < count; i++) {
381
0
                    LatValue *existing = (LatValue *)lat_map_get(parent, parts[i]);
382
0
                    if (existing && existing->type == VAL_MAP) {
383
0
                        parent = existing->as.map.map;
384
0
                    } else {
385
0
                        LatValue sub = value_map_new();
386
0
                        lat_map_set(parent, parts[i], &sub);
387
0
                        LatValue *ins = (LatValue *)lat_map_get(parent, parts[i]);
388
0
                        parent = ins->as.map.map;
389
0
                    }
390
0
                }
391
0
                char *arr_key = parts[count - 1];
392
0
                LatValue *arr_val = (LatValue *)lat_map_get(parent, arr_key);
393
0
                if (!arr_val || arr_val->type != VAL_ARRAY) {
394
0
                    LatValue empty_arr = value_array(NULL, 0);
395
0
                    lat_map_set(parent, arr_key, &empty_arr);
396
0
                    arr_val = (LatValue *)lat_map_get(parent, arr_key);
397
0
                }
398
                /* Push a new map entry to this array */
399
0
                LatValue new_entry = value_map_new();
400
0
                if (arr_val->as.array.len >= arr_val->as.array.cap) {
401
0
                    size_t old_cap = arr_val->as.array.cap;
402
0
                    arr_val->as.array.cap = old_cap < 4 ? 4 : old_cap * 2;
403
0
                    arr_val->as.array.elems = realloc(arr_val->as.array.elems,
404
0
                        arr_val->as.array.cap * sizeof(LatValue));
405
0
                }
406
0
                arr_val->as.array.elems[arr_val->as.array.len] = new_entry;
407
0
                current_table = arr_val->as.array.elems[arr_val->as.array.len].as.map.map;
408
0
                arr_val->as.array.len++;
409
3
            } else {
410
3
                if (p.src[p.pos] != ']') {
411
0
                    for (size_t i = 0; i < count; i++) free(parts[i]);
412
0
                    tp_error(&p, "expected ']'");
413
0
                    break;
414
0
                }
415
3
                p.pos++;
416
3
                current_table = tp_ensure_table(&root, parts, count);
417
3
            }
418
419
6
            for (size_t i = 0; i < count; i++) free(parts[i]);
420
3
            tp_skip_line_rest(&p);
421
3
            continue;
422
3
        }
423
424
        /* Key = Value */
425
27
        if (isalnum((unsigned char)p.src[p.pos]) || p.src[p.pos] == '"' ||
426
27
            p.src[p.pos] == '\'' || p.src[p.pos] == '_' || p.src[p.pos] == '-') {
427
428
27
            char *parts[32];
429
27
            size_t count = tp_parse_dotted_key(&p, parts, 32);
430
27
            if (count == 0 || p.err) {
431
0
                value_free(&root);
432
0
                *err = p.err ? p.err : strdup("toml_parse: invalid key");
433
0
                return value_unit();
434
0
            }
435
436
27
            tp_skip_ws(&p);
437
27
            if (p.src[p.pos] != '=') {
438
0
                for (size_t i = 0; i < count; i++) free(parts[i]);
439
0
                tp_error(&p, "expected '='");
440
0
                break;
441
0
            }
442
27
            p.pos++;
443
27
            tp_skip_ws(&p);
444
445
27
            LatValue val = tp_parse_value(&p);
446
27
            if (p.err) {
447
0
                for (size_t i = 0; i < count; i++) free(parts[i]);
448
0
                value_free(&val);
449
0
                break;
450
0
            }
451
452
            /* Navigate dotted key path */
453
27
            LatMap *target = current_table;
454
27
            for (size_t i = 0; i + 1 < count; i++) {
455
0
                LatValue *existing = (LatValue *)lat_map_get(target, parts[i]);
456
0
                if (existing && existing->type == VAL_MAP) {
457
0
                    target = existing->as.map.map;
458
0
                } else {
459
0
                    LatValue sub = value_map_new();
460
0
                    lat_map_set(target, parts[i], &sub);
461
0
                    LatValue *ins = (LatValue *)lat_map_get(target, parts[i]);
462
0
                    target = ins->as.map.map;
463
0
                }
464
0
            }
465
27
            lat_map_set(target, parts[count - 1], &val);
466
467
54
            for (size_t i = 0; i < count; i++) free(parts[i]);
468
27
            tp_skip_line_rest(&p);
469
27
            continue;
470
27
        }
471
472
0
        if (p.src[p.pos] == '#') { tp_skip_comment(&p); continue; }
473
0
        if (p.src[p.pos] == '\n' || p.src[p.pos] == '\r') { p.pos++; continue; }
474
475
0
        tp_error(&p, "unexpected character");
476
0
        break;
477
0
    }
478
479
15
    if (p.err) {
480
0
        value_free(&root);
481
0
        *err = p.err;
482
0
        return value_unit();
483
0
    }
484
485
15
    return root;
486
15
}
487
488
/* ========================================================================
489
 * TOML Stringify
490
 * ======================================================================== */
491
492
typedef struct {
493
    char  *buf;
494
    size_t len;
495
    size_t cap;
496
} TomlBuf;
497
498
3
static void tb_init(TomlBuf *b) {
499
3
    b->cap = 256;
500
3
    b->buf = malloc(b->cap);
501
3
    b->len = 0;
502
3
    b->buf[0] = '\0';
503
3
}
504
505
42
static void tb_append(TomlBuf *b, const char *s) {
506
42
    size_t slen = strlen(s);
507
42
    while (b->len + slen + 1 > b->cap) { b->cap *= 2; b->buf = realloc(b->buf, b->cap); }
508
42
    memcpy(b->buf + b->len, s, slen);
509
42
    b->len += slen;
510
42
    b->buf[b->len] = '\0';
511
42
}
512
513
static void tb_appendf(TomlBuf *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
514
3
static void tb_appendf(TomlBuf *b, const char *fmt, ...) {
515
3
    va_list ap;
516
3
    va_start(ap, fmt);
517
3
    char tmp[256];
518
3
    vsnprintf(tmp, sizeof(tmp), fmt, ap);
519
3
    va_end(ap);
520
3
    tb_append(b, tmp);
521
3
}
522
523
3
static void tb_append_escaped(TomlBuf *b, const char *s) {
524
3
    tb_append(b, "\"");
525
18
    while (*s) {
526
15
        switch (*s) {
527
0
            case '"': tb_append(b, "\\\""); break;
528
0
            case '\\': tb_append(b, "\\\\"); break;
529
0
            case '\n': tb_append(b, "\\n"); break;
530
0
            case '\t': tb_append(b, "\\t"); break;
531
0
            case '\r': tb_append(b, "\\r"); break;
532
15
            default: {
533
15
                char c[2] = { *s, '\0' };
534
15
                tb_append(b, c);
535
15
            }
536
15
        }
537
15
        s++;
538
15
    }
539
3
    tb_append(b, "\"");
540
3
}
541
542
static void toml_stringify_value(TomlBuf *b, const LatValue *val);
543
544
6
static void toml_stringify_value(TomlBuf *b, const LatValue *val) {
545
6
    switch (val->type) {
546
3
        case VAL_STR:
547
3
            tb_append_escaped(b, val->as.str_val);
548
3
            break;
549
3
        case VAL_INT:
550
3
            tb_appendf(b, "%lld", (long long)val->as.int_val);
551
3
            break;
552
0
        case VAL_FLOAT:
553
0
            tb_appendf(b, "%g", val->as.float_val);
554
0
            break;
555
0
        case VAL_BOOL:
556
0
            tb_append(b, val->as.bool_val ? "true" : "false");
557
0
            break;
558
0
        case VAL_ARRAY: {
559
0
            tb_append(b, "[");
560
0
            for (size_t i = 0; i < val->as.array.len; i++) {
561
0
                if (i > 0) tb_append(b, ", ");
562
0
                toml_stringify_value(b, &val->as.array.elems[i]);
563
0
            }
564
0
            tb_append(b, "]");
565
0
            break;
566
0
        }
567
0
        case VAL_MAP: {
568
0
            tb_append(b, "{");
569
0
            bool first = true;
570
0
            for (size_t i = 0; i < val->as.map.map->cap; i++) {
571
0
                if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue;
572
0
                if (!first) tb_append(b, ", ");
573
0
                first = false;
574
0
                tb_append(b, val->as.map.map->entries[i].key);
575
0
                tb_append(b, " = ");
576
0
                toml_stringify_value(b, (LatValue *)val->as.map.map->entries[i].value);
577
0
            }
578
0
            tb_append(b, "}");
579
0
            break;
580
0
        }
581
0
        default:
582
0
            tb_append(b, "\"\"");
583
0
            break;
584
6
    }
585
6
}
586
587
3
static void toml_stringify_table(TomlBuf *b, const LatValue *val, const char *prefix) {
588
3
    if (val->type != VAL_MAP) return;
589
590
    /* First pass: simple key = value */
591
51
    for (size_t i = 0; i < val->as.map.map->cap; i++) {
592
48
        if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue;
593
6
        LatValue *v = (LatValue *)val->as.map.map->entries[i].value;
594
6
        if (v->type == VAL_MAP) continue;
595
        /* Skip arrays of all-maps (array of tables) */
596
6
        if (v->type == VAL_ARRAY && v->as.array.len > 0) {
597
0
            bool all_maps = true;
598
0
            for (size_t j = 0; j < v->as.array.len; j++) {
599
0
                if (v->as.array.elems[j].type != VAL_MAP) { all_maps = false; break; }
600
0
            }
601
0
            if (all_maps) continue;
602
0
        }
603
6
        tb_append(b, val->as.map.map->entries[i].key);
604
6
        tb_append(b, " = ");
605
6
        toml_stringify_value(b, v);
606
6
        tb_append(b, "\n");
607
6
    }
608
609
    /* Second pass: sub-tables */
610
51
    for (size_t i = 0; i < val->as.map.map->cap; i++) {
611
48
        if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue;
612
6
        LatValue *v = (LatValue *)val->as.map.map->entries[i].value;
613
6
        const char *key = val->as.map.map->entries[i].key;
614
615
6
        if (v->type == VAL_MAP) {
616
0
            char *full_key;
617
0
            if (prefix[0] != '\0') {
618
0
                size_t flen = strlen(prefix) + strlen(key) + 2;
619
0
                full_key = malloc(flen);
620
0
                snprintf(full_key, flen, "%s.%s", prefix, key);
621
0
            } else {
622
0
                full_key = strdup(key);
623
0
            }
624
0
            tb_append(b, "\n[");
625
0
            tb_append(b, full_key);
626
0
            tb_append(b, "]\n");
627
0
            toml_stringify_table(b, v, full_key);
628
0
            free(full_key);
629
6
        } else if (v->type == VAL_ARRAY && v->as.array.len > 0) {
630
0
            bool all_maps = true;
631
0
            for (size_t j = 0; j < v->as.array.len; j++) {
632
0
                if (v->as.array.elems[j].type != VAL_MAP) { all_maps = false; break; }
633
0
            }
634
0
            if (all_maps) {
635
0
                char *full_key;
636
0
                if (prefix[0] != '\0') {
637
0
                    size_t flen = strlen(prefix) + strlen(key) + 2;
638
0
                    full_key = malloc(flen);
639
0
                    snprintf(full_key, flen, "%s.%s", prefix, key);
640
0
                } else {
641
0
                    full_key = strdup(key);
642
0
                }
643
0
                for (size_t j = 0; j < v->as.array.len; j++) {
644
0
                    tb_append(b, "\n[[");
645
0
                    tb_append(b, full_key);
646
0
                    tb_append(b, "]]\n");
647
0
                    toml_stringify_table(b, &v->as.array.elems[j], full_key);
648
0
                }
649
0
                free(full_key);
650
0
            }
651
0
        }
652
6
    }
653
3
}
654
655
6
char *toml_ops_stringify(const LatValue *val, char **err) {
656
6
    if (val->type != VAL_MAP) {
657
3
        *err = strdup("toml_stringify: value must be a Map");
658
3
        return NULL;
659
3
    }
660
3
    TomlBuf b;
661
3
    tb_init(&b);
662
3
    toml_stringify_table(&b, val, "");
663
3
    return b.buf;
664
6
}