WokML compiler code coverage report (LCOV)
Current view: top level - c - wok_diag.c (source / functions) Coverage Total Hit
Test: 084deda Lines: 98.2 % 113 111
Test Date: 2026-08-10 11:39:41 Functions: 92.9 % 14 13
Legend: Lines:     hit not hit

            Line data    Source code
       1              : /// [TODO]: Is there a way to use an dependency to make json output instead we do it ourself?
       2              : /// [TODO]: Is using json the best here? Or we can use msgpack here?
       3              : 
       4              : // wok_diag -- batching diagnostic sink implementation. See wok_diag.h for
       5              : // the contract: item granularity, cascade suppression at an already-reported
       6              : // offset, and a hard cap with a single trailing E-TOO-MANY.
       7              : 
       8              : #include "wok_diag.h"
       9              : 
      10              : #include <stdarg.h>
      11              : #include <stdbool.h>
      12              : #include <string.h>
      13              : 
      14              : // One entry per WokDiagCode, in declaration order (the enum is generated
      15              : // from the same X-macro, so the indices line up).
      16              : #define WOK_X(name, text) [name] = text,
      17              : static const char *const wok_diag_text_table[] = {WOK_DIAG_CODES(WOK_X)};
      18              : #undef WOK_X
      19              : 
      20              : static_assert(sizeof(wok_diag_text_table) / sizeof(wok_diag_text_table[0]) ==
      21              :                   WOK_DIAG_CODE_COUNT,
      22              :               "wok_diag text table must have one entry per WokDiagCode");
      23              : 
      24           92 : WOK_READONLY const char *wok_diag_code_text(WokDiagCode code) {
      25           92 :   return wok_diag_text_table[code];
      26              : }
      27              : 
      28              : struct WokDiagSink {
      29              :   WokArena *arena;
      30              :   const char *path;
      31              :   const char *src;
      32              :   usize src_len;
      33              : 
      34              :   WokDiag *diags;
      35              :   usize count;
      36              :   usize capacity;
      37              : 
      38              :   bool has_last_off;
      39              :   u32 last_off;
      40              : 
      41              :   bool too_many_emitted;
      42              : 
      43              :   u32 *line_starts;  // lazily built; nullptr until first use
      44              :   usize line_count;
      45              : };
      46              : 
      47        95502 : WokDiagSink *wok_diag_new(WokArena *arena, const char *path, const char *src,
      48              :                           usize src_len) {
      49        95502 :   WokDiagSink *s = WOK_NEW(arena, WokDiagSink);
      50        95502 :   s->arena = arena;
      51        95502 :   s->path = path;
      52        95502 :   s->src = src;
      53        95502 :   s->src_len = src_len;
      54        95502 :   s->diags = nullptr;
      55        95502 :   s->count = 0;
      56        95502 :   s->capacity = 0;
      57        95502 :   s->has_last_off = false;
      58        95502 :   s->last_off = 0;
      59        95502 :   s->too_many_emitted = false;
      60        95502 :   s->line_starts = nullptr;
      61        95502 :   s->line_count = 0;
      62        95502 :   return s;
      63              : }
      64              : 
      65       107024 : static void wok_diag_grow(WokDiagSink *s) {
      66       107024 :   if (s->count < s->capacity) return;
      67              : 
      68        34502 :   usize new_capacity = s->capacity == 0 ? (usize)4 : s->capacity * 2;
      69        34502 :   WokDiag *grown = WOK_NEW_N(s->arena, WokDiag, new_capacity);
      70        34502 :   if (s->count > 0) memcpy(grown, s->diags, s->count * sizeof(WokDiag));
      71        34502 :   s->diags = grown;
      72        34502 :   s->capacity = new_capacity;
      73              : }
      74              : 
      75       107024 : static void wok_diag_append(WokDiagSink *s, WokDiagCode code, u32 off,
      76              :                             u32 len, const char *msg) {
      77       107024 :   wok_diag_grow(s);
      78              : 
      79       107024 :   char *owned = wok_arena_copy(s->arena, msg, strlen(msg));
      80       107024 :   s->diags[s->count] = (WokDiag){.code = code, .off = off, .len = len, .msg = owned};
      81       107024 :   s->count += 1;
      82       107024 :   s->has_last_off = true;
      83       107024 :   s->last_off = off;
      84       107024 : }
      85              : 
      86       678387 : void wok_diag_add_text(WokDiagSink *s, WokDiagCode code, u32 off, u32 len,
      87              :                        const char *msg) {
      88       678387 :   if (s->too_many_emitted) return;
      89              : 
      90       110872 :   if (s->count >= WOK_DIAG_CAP) {
      91         3772 :     s->too_many_emitted = true;
      92         3772 :     wok_diag_append(s, WOK_E_TOO_MANY, off, 0, "too many errors; stopping here");
      93         3772 :     return;
      94              :   }
      95              : 
      96       107100 :   if (s->has_last_off && off == s->last_off) return;
      97              : 
      98       103252 :   wok_diag_append(s, code, off, len, msg);
      99              : }
     100              : 
     101       678380 : void wok_diag_add(WokDiagSink *s, WokDiagCode code, u32 off, u32 len,
     102              :                   const char *fmt, ...) {
     103       678380 :   char buf[512];
     104       678380 :   va_list ap;
     105       678380 :   va_start(ap, fmt);
     106       678380 :   (void)vsnprintf(buf, sizeof buf, fmt, ap);
     107       678380 :   va_end(ap);
     108              : 
     109       678380 :   wok_diag_add_text(s, code, off, len, buf);
     110       678380 : }
     111              : 
     112        93224 : usize wok_diag_count(const WokDiagSink *s) { return s->count; }
     113              : 
     114           87 : const WokDiag *wok_diag_at(const WokDiagSink *s, usize i) { return &s->diags[i]; }
     115              : 
     116            0 : bool wok_diag_full(const WokDiagSink *s) { return s->too_many_emitted; }
     117              : 
     118           94 : static void wok_diag_build_lines(WokDiagSink *s) {
     119           94 :   if (s->line_starts != nullptr) return;
     120              : 
     121              :   usize line_count = 1;
     122        11716 :   for (usize i = 0; i < s->src_len; i++) {
     123        11670 :     if (s->src[i] == '\n') line_count += 1;
     124              :   }
     125              : 
     126           46 :   u32 *starts = WOK_NEW_N(s->arena, u32, line_count);
     127           46 :   starts[0] = 0;
     128           46 :   usize idx = 1;
     129        11716 :   for (usize i = 0; i < s->src_len; i++) {
     130        11670 :     if (s->src[i] == '\n') {
     131          439 :       starts[idx] = (u32)(i + 1);
     132          439 :       idx += 1;
     133              :     }
     134              :   }
     135              : 
     136           46 :   s->line_starts = starts;
     137           46 :   s->line_count = line_count;
     138              : }
     139              : 
     140           94 : void wok_diag_position(const WokDiagSink *sink, u32 off, u32 *line,
     141              :                        u32 *col) {
     142              :   // The line table is a cache: build it on first use even though the sink
     143              :   // is observed through a const pointer here.
     144           94 :   WokDiagSink *s = (WokDiagSink *)sink;
     145           94 :   wok_diag_build_lines(s);
     146              : 
     147              :   // Rightmost line whose start is <= off. line_starts[0] == 0 always
     148              :   // satisfies this, so the search invariant holds from the first iteration;
     149              :   // an off past the end of the source simply resolves to the last line.
     150           94 :   usize lo = 0;
     151           94 :   usize hi = s->line_count;
     152          461 :   while (lo + 1 < hi) {
     153          273 :     usize mid = lo + (hi - lo) / 2;
     154          273 :     if (s->line_starts[mid] <= off) {
     155              :       lo = mid;
     156              :     } else {
     157          115 :       hi = mid;
     158              :     }
     159              :   }
     160              : 
     161           94 :   *line = (u32)(lo + 1);
     162           94 :   *col = off - s->line_starts[lo] + 1;
     163           94 : }
     164              : 
     165            3 : void wok_diag_render(const WokDiagSink *s, FILE *out) {
     166            6 :   for (usize i = 0; i < s->count; i++) {
     167            3 :     const WokDiag *d = &s->diags[i];
     168            3 :     u32 line;
     169            3 :     u32 col;
     170            3 :     wok_diag_position(s, d->off, &line, &col);
     171              :     // No code here, deliberately. The human-facing line stays
     172              :     // `file:line:col: message`, which is what an editor's error regex and a
     173              :     // reader both want; the machine-readable code is in the JSON Lines
     174              :     // renderer below, which is where a tool should be looking for it.
     175            3 :     (void)fprintf(out, "%s:%u:%u: %s\n", s->path, line, col, d->msg);
     176              :   }
     177            3 : }
     178              : 
     179              : // Emits a JSON string. It does NOT validate UTF-8: that happens once, when
     180              : // the source file is read, and every diagnostic message is built from ASCII
     181              : // literals plus text the scanner already validated. Two places used to break
     182              : // that invariant and both were fixed at the source rather than patched here --
     183              : // the scanner spelled an unknown escape with the raw byte, and the parser
     184              : // truncated a quoted token on a BYTE boundary, cutting characters in half on
     185              : // valid input. test/test_diag.c pins the invariant end to end.
     186          105 : static void wok_diag_json_string(FILE *out, const char *text) {
     187          105 :   (void)fputc('"', out);
     188         1190 :   for (const unsigned char *p = (const unsigned char *)text; *p != 0; p++) {
     189         1085 :     unsigned char c = *p;
     190         1085 :     switch (c) {
     191            3 :       case '"': (void)fputs("\\\"", out); break;
     192            2 :       case '\\': (void)fputs("\\\\", out); break;
     193            1 :       case '\n': (void)fputs("\\n", out); break;
     194            1 :       case '\t': (void)fputs("\\t", out); break;
     195            0 :       case '\r': (void)fputs("\\r", out); break;
     196         1078 :       default:
     197         1078 :         if (c < 0x20) (void)fprintf(out, "\\u%04x", (unsigned int)c);
     198         1078 :         else (void)fputc(c, out);
     199              :         break;
     200              :     }
     201              :   }
     202          105 :   (void)fputc('"', out);
     203          105 : }
     204              : 
     205           13 : void wok_diag_render_jsonl(const WokDiagSink *s, FILE *out) {
     206           48 :   for (usize i = 0; i < s->count; i++) {
     207           35 :     const WokDiag *d = &s->diags[i];
     208           35 :     u32 line;
     209           35 :     u32 col;
     210           35 :     wok_diag_position(s, d->off, &line, &col);
     211              : 
     212           35 :     (void)fputs("{\"file\":", out);
     213           35 :     wok_diag_json_string(out, s->path);
     214           35 :     (void)fprintf(out, ",\"line\":%u,\"col\":%u,\"code\":", line, col);
     215           35 :     wok_diag_json_string(out, wok_diag_code_text(d->code));
     216           35 :     (void)fputs(",\"message\":", out);
     217           35 :     wok_diag_json_string(out, d->msg);
     218           35 :     (void)fputs("}\n", out);  // one record per line: that is the format
     219              :   }
     220           13 : }
        

Generated by: LCOV version 2.4-1