WokML compiler code coverage report (LCOV)
Current view: top level - c - wok_sexpr.c (source / functions) Coverage Total Hit
Test: 084deda Lines: 80.2 % 485 389
Test Date: 2026-08-10 11:39:41 Functions: 100.0 % 36 36
Legend: Lines:     hit not hit

            Line data    Source code
       1              : // wok_sexpr -- see wok_sexpr.h for the format and the out_src contract.
       2              : //
       3              : // Every traversal here walks wok_node_desc[]; the only per-node-CLASS code
       4              : // is the seven-way switch on WokFieldClass (NODE/OPT/SEQ/NAME/TEXT/INT/FLAG),
       5              : // which is exactly the "generic" part of "generic dump and reader" -- adding
       6              : // a wok AST node never touches this file, only adding a new field CLASS
       7              : // would.
       8              : 
       9              : // This include a string buffer here.
      10              : 
      11              : // The prelude comes FIRST: it carries the POSIX feature-test macros, which
      12              : // have no effect once a system header has been read. wok_base.h hard-errors
      13              : // if it is reached too late.
      14              : #include "wok_base.h"
      15              : 
      16              : #include "wok_sexpr.h"
      17              : 
      18              : #include <inttypes.h>
      19              : #include <stdint.h>
      20              : #include <string.h>
      21              : 
      22              : // Real C call-stack depth, in both directions, is bounded by this: a node
      23              : // recurses into its NODE/OPT/SEQ fields, which recurse into their nodes, and
      24              : // so on. Past this depth we report rather than risk overflowing the stack.
      25              : #define WOK_SEXPR_MAX_DEPTH 256
      26              : 
      27              : // =========================================================================
      28              : // dump
      29              : // =========================================================================
      30              : 
      31              : // Both public dump entry points write through this so they are, by
      32              : // construction, the same algorithm: one to a FILE (stdio does its own
      33              : // buffering), one to an arena-backed growable buffer (doubling, like
      34              : // WokNodeBuf in wok_ast.c -- there is no realloc in an arena, so old copies
      35              : // are simply abandoned garbage, which is what an arena is for).
      36              : typedef struct {
      37              :   bool to_file;
      38              :   FILE *file;
      39              :   WokArena *arena;
      40              :   char *buf;
      41              :   usize len, cap;
      42              : } WokSexprSink;
      43              : 
      44     14211926 : static void sink_reserve(WokSexprSink *s, usize extra) {
      45     14211926 :   if (s->to_file) return;
      46     14211926 :   if (s->len + extra <= s->cap) return;
      47        88864 :   usize new_cap = s->cap ? s->cap * 2 : 128;
      48        88864 :   while (new_cap < s->len + extra) new_cap *= 2;
      49        88864 :   char *grown = WOK_NEW_N(s->arena, char, new_cap);
      50        88864 :   if (s->len) memcpy(grown, s->buf, s->len);
      51        88864 :   s->buf = grown;
      52        88864 :   s->cap = new_cap;
      53              : }
      54              : 
      55      4900523 : static void sink_put_char(WokSexprSink *s, char c) {
      56      4900523 :   if (s->to_file) {
      57          209 :     (void)fputc(c, s->file);
      58          209 :     return;
      59              :   }
      60      4900314 :   sink_reserve(s, 1);
      61      4900314 :   s->buf[s->len++] = c;
      62              : }
      63              : 
      64      9284164 : static void sink_put_str(WokSexprSink *s, const char *str, usize n) {
      65      9284164 :   if (s->to_file) {
      66          363 :     (void)fwrite(str, 1, n, s->file);
      67          363 :     return;
      68              :   }
      69      9283801 :   sink_reserve(s, n);
      70      9283801 :   memcpy(s->buf + s->len, str, n);
      71      9283801 :   s->len += n;
      72              : }
      73              : 
      74              : static void render_node(WokSexprSink *sink, const char *src, const WokNode *n,
      75              :                         u32 depth);
      76              : 
      77       675637 : static bool tag_is_block(WokTag t) {
      78       675637 :   const WokNodeDesc *d = &wok_node_desc[t];
      79      1210606 :   for (u16 i = 0; i < d->nfields; i++) {
      80       965251 :     WokFieldClass c = d->fields[i].cls;
      81       965251 :     if (c == WFC_NODE || c == WFC_OPT || c == WFC_SEQ) return true;
      82              :   }
      83              :   return false;
      84              : }
      85              : 
      86      1204150 : static void emit_indent(WokSexprSink *sink, u32 depth) {
      87      9210283 :   for (u32 i = 0; i < depth; i++) sink_put_str(sink, "  ", 2);
      88      1204150 : }
      89              : 
      90       360092 : static void render_string(WokSexprSink *sink, const char *src, WokSpan span) {
      91       360092 :   sink_put_char(sink, '"');
      92      1334084 :   for (u32 i = 0; i < span.len; i++) {
      93       973992 :     unsigned char c = (unsigned char)src[span.off + i];
      94       973992 :     switch (c) {
      95         7475 :       case '\\':
      96         7475 :         sink_put_str(sink, "\\\\", 2);
      97         7475 :         break;
      98        25370 :       case '"':
      99        25370 :         sink_put_str(sink, "\\\"", 2);
     100        25370 :         break;
     101            2 :       case '\n':
     102            2 :         sink_put_str(sink, "\\n", 2);
     103            2 :         break;
     104            2 :       case '\t':
     105            2 :         sink_put_str(sink, "\\t", 2);
     106            2 :         break;
     107            0 :       case '\r':
     108            0 :         sink_put_str(sink, "\\r", 2);
     109            0 :         break;
     110       941143 :       default:
     111       941143 :         if (c < 0x20) {
     112            2 :           char buf[5];
     113            2 :           (void)snprintf(buf, sizeof buf, "\\x%02X", (unsigned)c);
     114            2 :           sink_put_str(sink, buf, 4);
     115              :         } else {
     116       941141 :           sink_put_char(sink, (char)c);
     117              :         }
     118              :         break;
     119              :     }
     120              :   }
     121       360092 :   sink_put_char(sink, '"');
     122       360092 : }
     123              : 
     124        46735 : static void render_uint(WokSexprSink *sink, u64 v) {
     125        46735 :   char buf[32];
     126        46735 :   int len = snprintf(buf, sizeof buf, "%" PRIu64, v);
     127        46735 :   sink_put_str(sink, buf, (usize)len);
     128        46735 : }
     129              : 
     130              : // The one place a schema violation (a required NODE field left null) is
     131              : // caught: it cannot be dumped correctly, so this reports it and emits a
     132              : // placeholder that will, correctly, fail to read back.
     133       351385 : static void render_node_or_null(WokSexprSink *sink, const char *src,
     134              :                                 const char *parent_tag, const char *field,
     135              :                                 const WokNode *child, u32 depth) {
     136       351385 :   if (child == nullptr) {
     137            0 :     (void)fprintf(stderr,
     138              :                   "wok_sexpr: dump error: %s.%s is a null NODE field\n",
     139              :                   parent_tag, field);
     140            0 :     sink_put_str(sink, "(!null)", 7);
     141            0 :     return;
     142              :   }
     143       351385 :   render_node(sink, src, child, depth);
     144              : }
     145              : 
     146        22281 : static void render_opt(WokSexprSink *sink, const char *src,
     147              :                        const WokNode *child, u32 depth) {
     148        22281 :   if (child == nullptr) {
     149         7975 :     sink_put_str(sink, "(none)", 6);
     150         7975 :     return;
     151              :   }
     152        14306 :   sink_put_str(sink, "(some", 5);
     153        14306 :   sink_put_char(sink, '\n');
     154        14306 :   emit_indent(sink, depth + 1);
     155        14306 :   render_node(sink, src, child, depth + 1);
     156        14306 :   sink_put_char(sink, ')');
     157              : }
     158              : 
     159       273403 : static void render_seq(WokSexprSink *sink, const char *src, WokSeq seq,
     160              :                        u32 depth) {
     161       273403 :   if (seq.n == 0) {
     162        72427 :     sink_put_str(sink, "(seq)", 5);
     163        72427 :     return;
     164              :   }
     165       200976 :   sink_put_str(sink, "(seq", 4);
     166       516795 :   for (u32 i = 0; i < seq.n; i++) {
     167       315819 :     sink_put_char(sink, '\n');
     168       315819 :     emit_indent(sink, depth + 1);
     169       315819 :     render_node(sink, src, seq.items[i], depth + 1);
     170              :   }
     171       200976 :   sink_put_char(sink, ')');
     172              : }
     173              : 
     174      1247334 : static void render_field(WokSexprSink *sink, const char *src,
     175              :                          const char *parent_tag, const char *field_name,
     176              :                          WokFieldClass cls, WokSlot slot, u32 depth) {
     177      1247334 :   switch (cls) {
     178       351385 :     case WFC_NODE:
     179       351385 :       render_node_or_null(sink, src, parent_tag, field_name, slot.node,
     180              :                           depth);
     181       351385 :       return;
     182        22281 :     case WFC_OPT:
     183        22281 :       render_opt(sink, src, slot.node, depth);
     184        22281 :       return;
     185       273403 :     case WFC_SEQ:
     186       273403 :       render_seq(sink, src, wok_seq_unpack(slot.seq), depth);
     187       273403 :       return;
     188       360092 :     case WFC_NAME:
     189              :     case WFC_TEXT:
     190       360092 :       render_string(sink, src, slot.span);
     191       360092 :       return;
     192        46735 :     case WFC_INT:
     193        46735 :       render_uint(sink, slot.num);
     194        46735 :       return;
     195       193438 :     case WFC_FLAG:
     196       193438 :       sink_put_str(sink, slot.flag ? "#t" : "#f", 2);
     197       193438 :       return;
     198              :     case WOK_FIELD_CLASS_COUNT:
     199              :       break;
     200              :   }
     201            0 :   WOK_UNREACHABLE();
     202              : }
     203              : 
     204       709323 : static void render_node(WokSexprSink *sink, const char *src, const WokNode *n,
     205              :                         u32 depth) {
     206       709323 :   if (depth > WOK_SEXPR_MAX_DEPTH) {
     207            0 :     (void)fprintf(stderr,
     208              :                   "wok_sexpr: dump error: nesting exceeds %d at tag %s\n",
     209            0 :                   WOK_SEXPR_MAX_DEPTH, wok_node_desc[n->tag].tag);
     210            0 :     sink_put_str(sink, "(!depth)", 8);
     211            0 :     return;
     212              :   }
     213              : 
     214       709323 :   const WokNodeDesc *d = &wok_node_desc[n->tag];
     215       709323 :   sink_put_char(sink, '(');
     216       709323 :   sink_put_str(sink, d->tag, strlen(d->tag));
     217              : 
     218       709323 :   if (d->nfields == 0) {
     219        33686 :     sink_put_char(sink, ')');
     220        33686 :     return;
     221              :   }
     222              : 
     223       675637 :   if (!tag_is_block(n->tag)) {
     224       618664 :     for (u16 i = 0; i < d->nfields; i++) {
     225       373309 :       sink_put_char(sink, ' ');
     226       373309 :       render_field(sink, src, d->tag, d->fields[i].name, d->fields[i].cls,
     227              :                   n->slot[i], depth);
     228              :     }
     229       245355 :     sink_put_char(sink, ')');
     230       245355 :     return;
     231              :   }
     232              : 
     233      1304307 :   for (u16 i = 0; i < d->nfields; i++) {
     234       874025 :     sink_put_char(sink, '\n');
     235       874025 :     emit_indent(sink, depth + 1);
     236       874025 :     render_field(sink, src, d->tag, d->fields[i].name, d->fields[i].cls,
     237              :                 n->slot[i], depth + 1);
     238              :   }
     239       430282 :   sink_put_char(sink, ')');
     240              : }
     241              : 
     242            2 : void wok_sexpr_dump(const WokNode *node, const char *src, FILE *out) {
     243            2 :   WokSexprSink sink = {.to_file = true, .file = out};
     244            2 :   render_node(&sink, src, node, 1);
     245            2 :   (void)fputc('\n', out);
     246            2 : }
     247              : 
     248        27811 : char *wok_sexpr_dump_string(const WokNode *node, const char *src,
     249              :                             WokArena *arena) {
     250        27811 :   WokSexprSink sink = {.to_file = false, .arena = arena};
     251        27811 :   render_node(&sink, src, node, 1);
     252        27811 :   sink_put_char(&sink, '\n');
     253        27811 :   sink_reserve(&sink, 1);
     254        27811 :   sink.buf[sink.len] = '\0';
     255        27811 :   return sink.buf;
     256              : }
     257              : 
     258              : // =========================================================================
     259              : // read
     260              : // =========================================================================
     261              : 
     262              : // The decoded-text pool: NAME/TEXT spans in the returned tree index this,
     263              : // not the dump text (see wok_sexpr.h). Same doubling-in-the-arena growth as
     264              : // the dump-side sink above; kept separate because it holds decoded bytes
     265              : // under a different lifetime story (it outlives the read, the sink does
     266              : // not).
     267              : typedef struct {
     268              :   WokArena *arena;
     269              :   char *data;
     270              :   usize len, cap;
     271              : } WokByteBuf;
     272              : 
     273       350068 : static void bytebuf_reserve(WokByteBuf *b, usize extra) {
     274       350068 :   if (b->len + extra <= b->cap) return;
     275        11630 :   usize new_cap = b->cap ? b->cap * 2 : 128;
     276        11630 :   while (new_cap < b->len + extra) new_cap *= 2;
     277        11630 :   char *grown = WOK_NEW_N(b->arena, char, new_cap);
     278        11630 :   if (b->len) memcpy(grown, b->data, b->len);
     279        11630 :   b->data = grown;
     280        11630 :   b->cap = new_cap;
     281              : }
     282              : 
     283       339115 : static void bytebuf_put(WokByteBuf *b, char c) {
     284       339115 :   bytebuf_reserve(b, 1);
     285       339115 :   b->data[b->len++] = c;
     286       339115 : }
     287              : 
     288              : typedef struct {
     289              :   const char *text;
     290              :   usize n;
     291              :   usize pos;
     292              :   WokDiagSink *diag;
     293              :   WokArena *arena;
     294              :   WokByteBuf pool;
     295              : } WokReader;
     296              : 
     297     13468110 : static bool at_eof(const WokReader *r) { return r->pos >= r->n; }
     298     10946352 : static bool is_ws(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
     299      2617859 : static bool is_delim(char c) { return c == '(' || c == ')' || c == '"' || is_ws(c); }
     300              : 
     301      2304686 : static void skip_ws(WokReader *r) {
     302      8439773 :   while (!at_eof(r) && is_ws(r->text[r->pos])) r->pos++;
     303      2304686 : }
     304              : 
     305       892039 : static bool peek_is(WokReader *r, char c) {
     306       892039 :   skip_ws(r);
     307       892039 :   return !at_eof(r) && r->text[r->pos] == c;
     308              : }
     309              : 
     310       486142 : static bool expect_char(WokReader *r, char c, const char *what) {
     311       486142 :   skip_ws(r);
     312       486142 :   if (at_eof(r)) {
     313            0 :     wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     314              :                 "unterminated %s: expected `%c`, found end of input", what,
     315              :                 c);
     316            0 :     return false;
     317              :   }
     318       486142 :   if (r->text[r->pos] != c) {
     319            1 :     wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     320              :                 "expected `%c`, found `%c`", c, r->text[r->pos]);
     321            1 :     return false;
     322              :   }
     323       486141 :   r->pos++;
     324       486141 :   return true;
     325              : }
     326              : 
     327              : // A maximal run of non-delimiter characters: a tag, `#t`/`#f`, `seq`,
     328              : // `some`, `none`, or a decimal literal. Returns false (no diagnostic) when
     329              : // the next character is itself a delimiter or EOF -- the caller knows the
     330              : // context and reports accordingly.
     331       437125 : static bool read_word(WokReader *r, usize *out_off, usize *out_len) {
     332       437125 :   skip_ws(r);
     333       437125 :   usize start = r->pos;
     334      2617860 :   while (!at_eof(r) && !is_delim(r->text[r->pos])) r->pos++;
     335       437125 :   usize len = r->pos - start;
     336       437125 :   if (len == 0) return false;
     337       437125 :   *out_off = start;
     338       437125 :   *out_len = len;
     339       437125 :   return true;
     340              : }
     341              : 
     342       211415 : static bool word_eq(const WokReader *r, usize off, usize len,
     343              :                     const char *lit) {
     344       211415 :   usize litlen = strlen(lit);
     345       211415 :   return len == litlen && memcmp(r->text + off, lit, litlen) == 0;
     346              : }
     347              : 
     348            2 : static int hex_digit(char c) {
     349            2 :   if (c >= '0' && c <= '9') return c - '0';
     350            0 :   if (c >= 'a' && c <= 'f') return c - 'a' + 10;
     351            0 :   if (c >= 'A' && c <= 'F') return c - 'A' + 10;
     352              :   return -1;
     353              : }
     354              : 
     355              : // Decodes one quoted string literal at the current position, inverting
     356              : // render_string exactly. When pool is non-null the decoded bytes are
     357              : // appended there and *out_off/*out_len give the span into it; when pool is
     358              : // null the literal is only validated and skipped (used to count extra
     359              : // fields for a wrong-field-count diagnostic, where the decoded value is
     360              : // thrown away).
     361       124954 : static bool read_string_lit(WokReader *r, WokByteBuf *pool, u32 *out_off,
     362              :                             u32 *out_len) {
     363       124954 :   skip_ws(r);
     364       124954 :   usize start = r->pos;
     365       124954 :   if (!expect_char(r, '"', "string literal")) return false;
     366              : 
     367       124954 :   usize pool_start = pool ? pool->len : 0;
     368       464069 :   for (;;) {
     369       464069 :     if (at_eof(r)) {
     370            1 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)start, 0,
     371              :                   "unterminated string literal");
     372            1 :       return false;
     373              :     }
     374       464068 :     char c = r->text[r->pos];
     375       464068 :     if (c == '"') {
     376       124953 :       r->pos++;
     377       124953 :       break;
     378              :     }
     379       339115 :     if (c == '\\') {
     380        10974 :       r->pos++;
     381        10974 :       if (at_eof(r)) {
     382            0 :         wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)start, 0,
     383              :                     "unterminated string literal");
     384            0 :         return false;
     385              :       }
     386        10974 :       char e = r->text[r->pos];
     387        10974 :       char decoded;
     388        10974 :       switch (e) {
     389         2491 :         case '\\':
     390         2491 :           decoded = '\\';
     391         2491 :           r->pos++;
     392         2491 :           break;
     393         8480 :         case '"':
     394         8480 :           decoded = '"';
     395         8480 :           r->pos++;
     396         8480 :           break;
     397            1 :         case 'n':
     398            1 :           decoded = '\n';
     399            1 :           r->pos++;
     400            1 :           break;
     401            1 :         case 't':
     402            1 :           decoded = '\t';
     403            1 :           r->pos++;
     404            1 :           break;
     405            0 :         case 'r':
     406            0 :           decoded = '\r';
     407            0 :           r->pos++;
     408            0 :           break;
     409            1 :         case 'x': {
     410            1 :           r->pos++;
     411            1 :           if (r->pos + 2 > r->n) {
     412            0 :             wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)start, 0,
     413              :                         "unterminated \\x escape in string literal");
     414            0 :             return false;
     415              :           }
     416            1 :           int hi = hex_digit(r->text[r->pos]);
     417            1 :           int lo = hex_digit(r->text[r->pos + 1]);
     418            1 :           if (hi < 0 || lo < 0) {
     419            0 :             wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     420              :                         "invalid \\x escape in string literal");
     421            0 :             return false;
     422              :           }
     423            1 :           decoded = (char)((hi << 4) | lo);
     424            1 :           r->pos += 2;
     425            1 :           break;
     426              :         }
     427            0 :         default:
     428            0 :           wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     429              :                       "unknown escape `\\%c` in string literal", e);
     430            0 :           return false;
     431              :       }
     432        10974 :       if (pool) bytebuf_put(pool, decoded);
     433        10974 :       continue;
     434              :     }
     435       328141 :     if (pool) bytebuf_put(pool, c);
     436       328141 :     r->pos++;
     437              :   }
     438              : 
     439       124953 :   if (out_off) *out_off = (u32)pool_start;
     440       124953 :   if (out_len) *out_len = pool ? (u32)(pool->len - pool_start) : 0;
     441              :   return true;
     442              : }
     443              : 
     444        16167 : static bool read_uint_field(WokReader *r, u64 *out) {
     445        16167 :   usize off, len;
     446        16167 :   if (!read_word(r, &off, &len)) {
     447            0 :     if (at_eof(r)) {
     448            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     449              :                   "unterminated node: expected an integer field");
     450              :     } else {
     451            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     452            0 :                   "expected an integer field, found `%c`", r->text[r->pos]);
     453              :     }
     454            0 :     return false;
     455              :   }
     456              :   u64 v = 0;
     457        41713 :   for (usize i = 0; i < len; i++) {
     458        25546 :     char c = r->text[off + i];
     459        25546 :     if (c < '0' || c > '9') {
     460            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)off, (u32)len,
     461              :                   "expected a decimal integer, found `%.*s`", (int)len,
     462              :                   r->text + off);
     463            0 :       return false;
     464              :     }
     465        25546 :     u64 d = (u64)(c - '0');
     466        25546 :     if (v > (UINT64_MAX - d) / 10) {
     467            0 :       wok_diag_add(r->diag, WOK_E_LEX_INT_RANGE, (u32)off, (u32)len,
     468              :                   "integer literal `%.*s` out of range", (int)len,
     469              :                   r->text + off);
     470            0 :       return false;
     471              :     }
     472        25546 :     v = v * 10 + d;
     473              :   }
     474        16167 :   *out = v;
     475        16167 :   return true;
     476              : }
     477              : 
     478        67486 : static bool read_flag_field(WokReader *r, bool *out) {
     479        67486 :   usize off, len;
     480        67486 :   if (!read_word(r, &off, &len)) {
     481            0 :     if (at_eof(r)) {
     482            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     483              :                   "unterminated node: expected `#t` or `#f`");
     484              :     } else {
     485            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     486            0 :                   "expected `#t` or `#f`, found `%c`", r->text[r->pos]);
     487              :     }
     488            0 :     return false;
     489              :   }
     490        67486 :   if (word_eq(r, off, len, "#t")) {
     491        33175 :     *out = true;
     492        33175 :     return true;
     493              :   }
     494        34311 :   if (word_eq(r, off, len, "#f")) {
     495        34311 :     *out = false;
     496        34311 :     return true;
     497              :   }
     498            0 :   wok_diag_add(r->diag, WOK_E_PARSE, (u32)off, (u32)len,
     499            0 :               "expected `#t` or `#f`, found `%.*s`", (int)len, r->text + off);
     500            0 :   return false;
     501              : }
     502              : 
     503       248676 : static bool lookup_tag(const WokReader *r, usize off, usize len,
     504              :                        WokTag *out) {
     505      7318603 :   for (int t = 0; t < WOK_TAG_COUNT; t++) {
     506      7318602 :     const char *name = wok_node_desc[t].tag;
     507      7318602 :     usize nl = strlen(name);
     508      7318602 :     if (nl == len && memcmp(r->text + off, name, len) == 0) {
     509       248675 :       *out = (WokTag)t;
     510       248675 :       return true;
     511              :     }
     512              :   }
     513              :   return false;
     514              : }
     515              : 
     516              : static WokNode *parse_node(WokReader *r, u32 depth);
     517              : 
     518              : // Discards one opaque atom or balanced list, used only to keep counting
     519              : // fields after a node has already overrun its declared arity, so the
     520              : // "expected N, got M" diagnostic can name a real M instead of guessing.
     521            1 : static bool skip_one_form(WokReader *r) {
     522            1 :   skip_ws(r);
     523            1 :   if (at_eof(r)) {
     524            0 :     wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     525              :                 "unterminated list");
     526            0 :     return false;
     527              :   }
     528            1 :   char c = r->text[r->pos];
     529            1 :   if (c == '"') return read_string_lit(r, nullptr, nullptr, nullptr);
     530            1 :   if (c == '(') {
     531            0 :     usize start = r->pos;
     532            0 :     r->pos++;
     533            0 :     int nesting = 1;
     534            0 :     while (nesting > 0) {
     535            0 :       if (at_eof(r)) {
     536            0 :         wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)start, 0,
     537              :                     "unterminated list");
     538            0 :         return false;
     539              :       }
     540            0 :       char cc = r->text[r->pos];
     541            0 :       if (cc == '"') {
     542            0 :         if (!read_string_lit(r, nullptr, nullptr, nullptr)) return false;
     543            0 :         continue;
     544              :       }
     545            0 :       if (cc == '(') nesting++;
     546            0 :       else if (cc == ')') nesting--;
     547            0 :       r->pos++;
     548              :     }
     549              :     return true;
     550              :   }
     551            1 :   if (c == ')') return false;
     552            1 :   usize off, len;
     553            1 :   return read_word(r, &off, &len);
     554              : }
     555              : 
     556         7716 : static bool parse_opt(WokReader *r, u32 depth, WokNode **out) {
     557         7716 :   skip_ws(r);
     558         7716 :   usize off = r->pos;
     559         7716 :   if (!expect_char(r, '(', "OPT field")) return false;
     560              : 
     561         7716 :   usize woff, wlen;
     562         7716 :   if (!read_word(r, &woff, &wlen)) {
     563            0 :     if (at_eof(r)) {
     564            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)off, 0,
     565              :                   "unterminated OPT field: expected `none` or `some`");
     566              :     } else {
     567            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     568              :                   "expected `none` or `some`");
     569              :     }
     570            0 :     return false;
     571              :   }
     572         7716 :   if (word_eq(r, woff, wlen, "none")) {
     573         2893 :     if (!expect_char(r, ')', "none")) return false;
     574         2893 :     *out = nullptr;
     575         2893 :     return true;
     576              :   }
     577         4823 :   if (word_eq(r, woff, wlen, "some")) {
     578         4823 :     WokNode *child = parse_node(r, depth + 1);
     579         4823 :     if (!child) return false;
     580         4823 :     if (!expect_char(r, ')', "some")) return false;
     581         4823 :     *out = child;
     582         4823 :     return true;
     583              :   }
     584            0 :   wok_diag_add(r->diag, WOK_E_PARSE, (u32)woff, (u32)wlen,
     585              :               "expected `none` or `some`, found `%.*s`", (int)wlen,
     586            0 :               r->text + woff);
     587            0 :   return false;
     588              : }
     589              : 
     590        97080 : static bool parse_seq(WokReader *r, u32 depth, WokSeq *out) {
     591        97080 :   skip_ws(r);
     592        97080 :   usize off = r->pos;
     593        97080 :   if (!expect_char(r, '(', "SEQ field")) return false;
     594              : 
     595        97079 :   usize woff, wlen;
     596        97079 :   if (!read_word(r, &woff, &wlen)) {
     597            0 :     if (at_eof(r)) {
     598            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)off, 0,
     599              :                   "unterminated SEQ field: expected `seq`");
     600              :     } else {
     601            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     602              :                   "expected `seq`");
     603              :     }
     604            0 :     return false;
     605              :   }
     606        97079 :   if (!word_eq(r, woff, wlen, "seq")) {
     607            0 :     wok_diag_add(r->diag, WOK_E_PARSE, (u32)woff, (u32)wlen,
     608            0 :                 "expected `seq`, found `%.*s`", (int)wlen, r->text + woff);
     609            0 :     return false;
     610              :   }
     611              : 
     612        97079 :   WokNodeBuf buf;
     613        97079 :   wok_buf_init(&buf, r->arena);
     614       109852 :   for (;;) {
     615       206931 :     if (peek_is(r, ')')) {
     616        97076 :       r->pos++;
     617        97076 :       break;
     618              :     }
     619       109855 :     if (at_eof(r)) {
     620            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     621              :                   "unterminated seq");
     622            0 :       return false;
     623              :     }
     624       109855 :     WokNode *child = parse_node(r, depth + 1);
     625       109855 :     if (!child) return false;
     626       109852 :     wok_buf_push(&buf, child);
     627              :   }
     628        97076 :   *out = wok_buf_seq(&buf);
     629        97076 :   return true;
     630              : }
     631              : 
     632              : // THE FAMILY CHECK. The schema says which kind of child a field demands, so a
     633              : // dump holding a type where an expression belongs is REPORTED here rather than
     634              : // read back as a different program. The class check alone let it through: it
     635              : // asks whether a child is present, never what the child IS.
     636       237711 : static bool family_ok(WokReader *r, const WokNode *child, WokFamily want,
     637              :                       const char *field) {
     638       237711 :   WokFamily got = wok_node_desc[child->tag].family;
     639       237711 :   if (wok_family_accepts(want, got)) return true;
     640            3 :   wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     641              :                "field `%s` takes %s, found %s, which is %s", field,
     642            3 :                wok_family_name(want), wok_node_desc[child->tag].tag,
     643              :                wok_family_name(got));
     644            3 :   return false;
     645              : }
     646              : 
     647       436440 : static bool parse_field(WokReader *r, WokNode *node, u16 i,
     648              :                         const WokFieldDesc *desc, u32 depth) {
     649       436440 :   WokFieldClass cls = desc->cls;
     650       436440 :   switch (cls) {
     651       123037 :     case WFC_NODE: {
     652       123037 :       WokNode *child = parse_node(r, depth + 1);
     653       123037 :       if (!child) return false;
     654       123036 :       if (!family_ok(r, child, desc->family, desc->name)) return false;
     655       123034 :       node->slot[i] = WOK_MK_NODE(child);
     656       123034 :       return true;
     657              :     }
     658         7716 :     case WFC_OPT: {
     659         7716 :       WokNode *child = nullptr;
     660         7716 :       if (!parse_opt(r, depth, &child)) return false;
     661         7716 :       if (child && !family_ok(r, child, desc->family, desc->name)) return false;
     662         7716 :       node->slot[i] = WOK_MK_OPT(child);
     663         7716 :       return true;
     664              :     }
     665        97080 :     case WFC_SEQ: {
     666        97080 :       WokSeq seq;
     667        97080 :       if (!parse_seq(r, depth, &seq)) return false;
     668       206927 :       for (u32 k = 0; k < seq.n; k++)
     669       109852 :         if (!family_ok(r, seq.items[k], desc->family, desc->name)) return false;
     670        97075 :       node->slot[i] = WOK_MK_SEQ(seq);
     671        97075 :       return true;
     672              :     }
     673       117930 :     case WFC_NAME: {
     674       117930 :       u32 off, len;
     675       117930 :       if (!read_string_lit(r, &r->pool, &off, &len)) return false;
     676       117930 :       node->slot[i] = WOK_MK_NAME(wok_span(off, len));
     677       117930 :       return true;
     678              :     }
     679         7024 :     case WFC_TEXT: {
     680         7024 :       u32 off, len;
     681         7024 :       if (!read_string_lit(r, &r->pool, &off, &len)) return false;
     682         7023 :       node->slot[i] = WOK_MK_TEXT(wok_span(off, len));
     683         7023 :       return true;
     684              :     }
     685        16167 :     case WFC_INT: {
     686        16167 :       u64 v;
     687        16167 :       if (!read_uint_field(r, &v)) return false;
     688        16167 :       node->slot[i] = WOK_MK_INT(v);
     689        16167 :       return true;
     690              :     }
     691        67486 :     case WFC_FLAG: {
     692        67486 :       bool v;
     693        67486 :       if (!read_flag_field(r, &v)) return false;
     694        67486 :       node->slot[i] = WOK_MK_FLAG(v);
     695        67486 :       return true;
     696              :     }
     697              :     case WOK_FIELD_CLASS_COUNT:
     698              :       break;
     699              :   }
     700            0 :   WOK_UNREACHABLE();
     701              : }
     702              : 
     703       248676 : static WokNode *parse_node(WokReader *r, u32 depth) {
     704       248676 :   if (depth > WOK_SEXPR_MAX_DEPTH) {
     705            0 :     wok_diag_add(r->diag, WOK_E_DEPTH, (u32)r->pos, 0,
     706              :                 "s-expression nesting exceeds %d levels", WOK_SEXPR_MAX_DEPTH);
     707            0 :     return nullptr;
     708              :   }
     709              : 
     710       248676 :   skip_ws(r);
     711       248676 :   usize open_off = r->pos;
     712       248676 :   if (!expect_char(r, '(', "node")) return nullptr;
     713              : 
     714       248676 :   usize tag_off, tag_len;
     715       248676 :   if (!read_word(r, &tag_off, &tag_len)) {
     716            0 :     if (at_eof(r)) {
     717            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)open_off, 0,
     718              :                   "unterminated node: missing tag");
     719              :     } else {
     720            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     721              :                   "expected a tag name after `(`");
     722              :     }
     723            0 :     return nullptr;
     724              :   }
     725              : 
     726       248676 :   WokTag tag;
     727       248676 :   if (!lookup_tag(r, tag_off, tag_len, &tag)) {
     728            1 :     wok_diag_add(r->diag, WOK_E_PARSE, (u32)tag_off, (u32)tag_len,
     729            1 :                 "unknown tag `%.*s`", (int)tag_len, r->text + tag_off);
     730            1 :     return nullptr;
     731              :   }
     732              : 
     733       248675 :   const WokNodeDesc *desc = &wok_node_desc[tag];
     734       248675 :   WokNode *node = wok_node(r->arena, tag, 0, 0);
     735              : 
     736       685106 :   for (u16 i = 0; i < desc->nfields; i++) {
     737       436440 :     if (peek_is(r, ')')) {
     738            0 :       wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     739            0 :                   "%s: expected %u fields, got %u", desc->tag,
     740              :                   (unsigned)desc->nfields, (unsigned)i);
     741            0 :       return nullptr;
     742              :     }
     743       436440 :     if (at_eof(r)) {
     744            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     745            0 :                   "unterminated node `%s`", desc->tag);
     746            0 :       return nullptr;
     747              :     }
     748       436440 :     if (!parse_field(r, node, i, &desc->fields[i], depth)) return nullptr;
     749              :   }
     750              : 
     751       248666 :   if (!peek_is(r, ')')) {
     752            2 :     if (at_eof(r)) {
     753            1 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     754            1 :                   "unterminated node `%s`: missing `)`", desc->tag);
     755            1 :       return nullptr;
     756              :     }
     757              :     u16 extra = 0;
     758            2 :     while (!peek_is(r, ')') && !at_eof(r)) {
     759            1 :       if (!skip_one_form(r)) return nullptr;
     760            1 :       extra++;
     761              :     }
     762            1 :     if (at_eof(r)) {
     763            0 :       wok_diag_add(r->diag, WOK_E_LEX_UNTERMINATED, (u32)r->pos, 0,
     764            0 :                   "unterminated node `%s`: missing `)`", desc->tag);
     765            0 :       return nullptr;
     766              :     }
     767            1 :     wok_diag_add(r->diag, WOK_E_PARSE, (u32)r->pos, 0,
     768            1 :                 "%s: expected %u fields, got %u", desc->tag,
     769            1 :                 (unsigned)desc->nfields, (unsigned)(desc->nfields + extra));
     770            1 :     return nullptr;
     771              :   }
     772       248664 :   r->pos++;  // consume ')'
     773       248664 :   return node;
     774              : }
     775              : 
     776        10961 : WokNode *wok_sexpr_read(const char *text, usize n, WokArena *arena,
     777              :                         WokDiagSink *diag, const char **out_src) {
     778        10961 :   WokReader r = {.text = text,
     779              :                 .n = n,
     780              :                 .pos = 0,
     781              :                 .diag = diag,
     782              :                 .arena = arena,
     783              :                 .pool = {.arena = arena}};
     784              : 
     785        10961 :   WokNode *root = parse_node(&r, 1);
     786        10961 :   if (root) {
     787        10953 :     skip_ws(&r);
     788        10953 :     if (!at_eof(&r)) {
     789            0 :       wok_diag_add(diag, WOK_E_PARSE, (u32)r.pos, 0,
     790              :                   "trailing content after the top-level form");
     791            0 :       root = nullptr;
     792              :     }
     793              :   }
     794              : 
     795            0 :   if (!root) {
     796            8 :     if (out_src) *out_src = nullptr;
     797            8 :     return nullptr;
     798              :   }
     799              : 
     800        10953 :   bytebuf_reserve(&r.pool, 1);
     801        10953 :   r.pool.data[r.pool.len] = '\0';
     802        10953 :   if (out_src) *out_src = r.pool.data;
     803              :   return root;
     804              : }
        

Generated by: LCOV version 2.4-1


Machine-readable coverage data

This page is generated. If you are a tool, a script, or an LLM, read the JSON instead of scraping this HTML — it is the same measurement, exact, and it names the individual uncovered lines.

Every artifact here is stamped with the commit it measured and the run that produced it: commit 084deda, dated 2026-08-10T11:39:41Z, built by Github CI run. The totals in coverage.json are checked against lcov’s own summary before publishing, so the JSON and this page cannot disagree.