diff --git a/src/kdl/ast.h b/src/kdl/ast.h index 2ee67a0..9660e85 100644 --- a/src/kdl/ast.h +++ b/src/kdl/ast.h @@ -56,14 +56,18 @@ struct st_kdl_token_ref { /* A positional argument, in source order (spec mandates order). */ struct st_kdl_arg { struct st_kdl_token_ref value; + struct st_kdl_token_ref *annotation; /* NULL when not annotated */ struct st_kdl_arg *next; }; /* A `key = value` property, in source order. The key is a string-ish - * token (identifier or any string form); the value is any value token. */ + * token (identifier or any string form); the value is any value token. + * `annotation` is the value's type annotation (ST_TOK_TYPE), e.g. the + * `u8` in `key=(u8)42`; NULL when the value is not annotated. */ struct st_kdl_prop { struct st_kdl_token_ref key; struct st_kdl_token_ref value; + struct st_kdl_token_ref *annotation; /* NULL when not annotated */ struct st_kdl_prop *next; }; diff --git a/src/kdl/parser.c b/src/kdl/parser.c index 7c69fc1..dbaba13 100644 --- a/src/kdl/parser.c +++ b/src/kdl/parser.c @@ -218,7 +218,7 @@ ref_from_token(struct st_kdl_token_ref *ref, const struct st_token *t) } static struct st_kdl_arg * -arg_new(const struct st_token *t) +arg_new(const struct st_token *t, const struct st_token *ann, bool has_ann) { struct st_kdl_arg *a = calloc(1, sizeof(*a)); @@ -226,11 +226,20 @@ arg_new(const struct st_token *t) return NULL; } ref_from_token(&a->value, t); + if (has_ann) { + a->annotation = malloc(sizeof(*a->annotation)); + if (a->annotation == NULL) { + free(a); + return NULL; + } + ref_from_token(a->annotation, ann); + } return a; } static struct st_kdl_prop * -prop_new(const struct st_token *key, const struct st_token *value) +prop_new(const struct st_token *key, const struct st_token *value, + const struct st_token *ann, bool has_ann) { struct st_kdl_prop *pr = calloc(1, sizeof(*pr)); @@ -239,6 +248,14 @@ prop_new(const struct st_token *key, const struct st_token *value) } ref_from_token(&pr->key, key); ref_from_token(&pr->value, value); + if (has_ann) { + pr->annotation = malloc(sizeof(*pr->annotation)); + if (pr->annotation == NULL) { + free(pr); + return NULL; + } + ref_from_token(pr->annotation, ann); + } return pr; } @@ -278,11 +295,13 @@ node_free(struct st_kdl_node *n) free(n->type); for (a = n->args; a != NULL;) { struct st_kdl_arg *nx = a->next; + free(a->annotation); free(a); a = nx; } for (pr = n->props; pr != NULL;) { struct st_kdl_prop *px = pr->next; + free(pr->annotation); free(pr); pr = px; } @@ -444,12 +463,45 @@ parse_node(struct st_parser *p, struct st_error **err) *err = err_at_owned(p->tok.span, "unexpected '='"); node_free(n); return NULL; - case ST_TOK_TYPE: - *err = err_at_owned(p->tok.span, - "unexpected type annotation (value annotations are " - "not supported yet)"); - node_free(n); - return NULL; + case ST_TOK_TYPE: { + /* Value annotation: capture it, then require an argument value. + * (A type annotation in entry position prefixes a VALUE, so the + * annotated token is always an argument, never a property key.) */ + struct st_token ann_tok = p->tok; + struct st_kdl_arg *a; + + p_consume(p); + if (skip_comments(p, err) < 0) { + node_free(n); + return NULL; + } + k = p_peek(p, err); + if (k == ST_TOK_ERROR) { + node_free(n); + return NULL; + } + if (!tok_is_value(k)) { + *err = err_at_owned(p->tok.span, + "expected a value after type annotation"); + node_free(n); + return NULL; + } + a = arg_new(&p->tok, &ann_tok, true); + if (a == NULL) { + *err = err_at_owned(p->tok.span, "out of memory"); + node_free(n); + return NULL; + } + last_line = p->tok.span.line; + p_consume(p); + if (args_tail == NULL) { + n->args = args_tail = a; + } else { + args_tail->next = a; + args_tail = a; + } + continue; + } default: if (!tok_is_value(k)) { *err = err_at_owned(p->tok.span, @@ -491,13 +543,38 @@ parse_node(struct st_parser *p, struct st_error **err) node_free(n); return NULL; } - if (!tok_is_value(k)) { - *err = err_at_owned(p->tok.span, - "expected a value after '='"); - node_free(n); - return NULL; + { + struct st_token vann_tok = { 0 }; + bool has_vann = false; + + if (k == ST_TOK_TYPE) { + vann_tok = p->tok; + has_vann = true; + p_consume(p); + if (skip_comments(p, err) < 0) { + node_free(n); + return NULL; + } + k = p_peek(p, err); + if (k == ST_TOK_ERROR) { + node_free(n); + return NULL; + } + if (!tok_is_value(k)) { + *err = err_at_owned(p->tok.span, + "expected a value after type annotation"); + node_free(n); + return NULL; + } + } + if (!tok_is_value(k)) { + *err = err_at_owned(p->tok.span, + "expected a value after '='"); + node_free(n); + return NULL; + } + pr = prop_new(&val_tok, &p->tok, &vann_tok, has_vann); } - pr = prop_new(&val_tok, &p->tok); if (pr == NULL) { *err = err_at_owned(val_tok.span, "out of memory"); node_free(n); @@ -516,7 +593,7 @@ parse_node(struct st_parser *p, struct st_error **err) } /* Argument. */ { - struct st_kdl_arg *a = arg_new(&val_tok); + struct st_kdl_arg *a = arg_new(&val_tok, NULL, false); if (a == NULL) { *err = err_at_owned(val_tok.span, "out of memory"); diff --git a/src/kdl/value.c b/src/kdl/value.c new file mode 100644 index 0000000..262fa8a --- /dev/null +++ b/src/kdl/value.c @@ -0,0 +1,1053 @@ +/* + * value.c - typed KDL value model for stupidtools (todo 8). + * + * Interprets raw token refs (src/kdl/ast.h) into typed values. See value.h + * for the API contract and the documented subset. The interesting parts: + * + * - integer parsing: radix prefixes (0x/0o/0b), underscores, sign, + * overflow-guarded accumulation into u64 then range-checked to i64/u64. + * - float parsing: hand-rolled decimal accumulation (no strtod, no libm); + * 10^exponent via exponentiation-by-squaring so `1e10` needs no pow(). + * - string unescaping: KDL escapes + a lenient superset; whitespace + * escapes; unicode scalar-value validation + UTF-8 encoding. + * - multi-line strings: newline normalization, leading-newline removal, + * and common-indent dedent by the closing line's whitespace. + * + * DOCUMENTED SIMPLIFICATIONS (mirrored in learnings.md): + * - Multi-line strings dedent BEFORE resolving escapes. The spec resolves + * whitespace escapes first, then dedents, then resolves the rest; our + * order differs only when a whitespace escape would change the line + * structure used for dedent (a rare, pathological case). Literal + * newline sequences are normalized to LF; `\n` escapes are untouched by + * dedent and resolve to newlines afterward. + * - A content line that does not start with the exact closing-line prefix + * is emitted verbatim (lenient) rather than rejected, and a multi-line + * token without the spec's mandatory leading newline is treated as a + * single un-dedented line (so `"""multi"""` -> "multi"). + * - `\0` is rejected: KDL forbids U+0000 and the value is a NUL-terminated + * C string. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "kdl/value.h" + +#include "error.h" +#include "kdl/ast.h" +#include "kdl/lexer.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ---- owned errors ----------------------------------------------------- */ + +/* Build an owned ST_ERR_KDL_PARSE error whose span lives in the same + * allocation as the error struct (aligned right after it), mirroring the + * parser's err_at_owned. Returns NULL only on allocation failure. */ +static struct st_error * +err_at_owned(struct st_span sp, const char *msg) +{ + const size_t align = _Alignof(struct st_span); + const size_t esize = + (sizeof(struct st_error) + align - 1) & ~(align - 1); + struct st_error *e; + struct st_span *spc; + size_t mlen; + + if (msg == NULL) { + msg = ""; + } + e = malloc(esize + sizeof(struct st_span)); + if (e == NULL) { + return NULL; + } + mlen = strlen(msg); + e->message = malloc(mlen + 1); + if (e->message == NULL) { + free(e); + return NULL; + } + memcpy(e->message, msg, mlen + 1); + e->category = ST_ERR_KDL_PARSE; + spc = (struct st_span *)((unsigned char *)e + esize); + *spc = sp; + e->span = spc; + return e; +} + +/* ---- growable string buffer ------------------------------------------- */ + +struct sbuf { + char *data; /* NUL-terminated once any bytes are appended */ + size_t len; + size_t cap; +}; + +static int +sbuf_reserve(struct sbuf *sb, size_t extra) +{ + if (sb->len + extra + 1 > sb->cap) { + size_t ncap = sb->cap != 0 ? sb->cap * 2 : 64; + char *nd; + + while (ncap < sb->len + extra + 1) { + ncap *= 2; + } + nd = realloc(sb->data, ncap); + if (nd == NULL) { + return -1; + } + sb->data = nd; + sb->cap = ncap; + } + return 0; +} + +static int +sbuf_append(struct sbuf *sb, const char *p, size_t n) +{ + if (sbuf_reserve(sb, n) < 0) { + return -1; + } + if (n > 0) { + memcpy(sb->data + sb->len, p, n); + } + sb->len += n; + sb->data[sb->len] = '\0'; + return 0; +} + +static int +sbuf_putc(struct sbuf *sb, char c) +{ + return sbuf_append(sb, &c, 1); +} + +static void +sbuf_free(struct sbuf *sb) +{ + free(sb->data); + sb->data = NULL; + sb->len = 0; + sb->cap = 0; +} + +/* ---- character classification ----------------------------------------- */ + +static bool +is_digit_c(char c) +{ + return c >= '0' && c <= '9'; +} + +static bool +is_hex_digit_c(char c) +{ + return is_digit_c(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static int +digit_value(char c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; +} + +/* Length of the newline sequence at p (0 if none), matching the lexer's + * is_newline_at: CRLF, CR, LF, VT, FF, NEL, LS, PS. */ +static size_t +newline_len(const char *p, const char *end) +{ + unsigned char c; + + if (p >= end) { + return 0; + } + c = (unsigned char)p[0]; + if (c == '\r') { + return (p + 1 < end && p[1] == '\n') ? 2 : 1; + } + if (c == '\n' || c == '\v' || c == '\f') { + return 1; + } + if (c == 0xC2 && p + 1 < end && (unsigned char)p[1] == 0x85) { + return 2; + } + if (c == 0xE2 && p + 2 < end && (unsigned char)p[1] == 0x80 && + ((unsigned char)p[2] == 0xA8 || (unsigned char)p[2] == 0xA9)) { + return 3; + } + return 0; +} + +static bool +is_whitespace_only(const char *s, const char *e) +{ + for (; s < e; s++) { + if (*s != ' ' && *s != '\t') { + return false; + } + } + return true; +} + +/* ---- owned string helpers --------------------------------------------- */ + +/* Copy a length-bounded (possibly non-NUL-terminated) slice into a fresh + * NUL-terminated heap string. */ +static char * +slice_dup(const char *s, size_t n) +{ + char *d = malloc(n + 1); + + if (d == NULL) { + return NULL; + } + memcpy(d, s, n); + d[n] = '\0'; + return d; +} + +/* Steal `sb`'s buffer into out->as.str (allocating "" when empty) and mark + * the value as a string. On OOM returns an owned error. */ +static struct st_error * +finish_string(struct sbuf *sb, struct st_kdl_value *out, struct st_span span) +{ + if (sb->data == NULL) { + out->as.str = malloc(1); + if (out->as.str == NULL) { + return err_at_owned(span, "out of memory"); + } + out->as.str[0] = '\0'; + } else { + out->as.str = sb->data; + sb->data = NULL; + } + out->kind = ST_KDL_VAL_STRING; + return NULL; +} + +/* ---- unescaping ------------------------------------------------------- */ + +/* Append a UTF-8 encoding of `cp` (a valid Unicode scalar value). */ +static int +sbuf_put_utf8(struct sbuf *sb, unsigned long cp) +{ + char buf[4]; + int n; + + if (cp < 0x80) { + buf[0] = (char)cp; + n = 1; + } else if (cp < 0x800) { + buf[0] = (char)(0xC0 | (cp >> 6)); + buf[1] = (char)(0x80 | (cp & 0x3F)); + n = 2; + } else if (cp < 0x10000) { + buf[0] = (char)(0xE0 | (cp >> 12)); + buf[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[2] = (char)(0x80 | (cp & 0x3F)); + n = 3; + } else { + buf[0] = (char)(0xF0 | (cp >> 18)); + buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); + buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[3] = (char)(0x80 | (cp & 0x3F)); + n = 4; + } + return sbuf_append(sb, buf, (size_t)n); +} + +/* Append the unescaped form of [p, end) to sb. Returns NULL on success or + * an owned error (with `span`) on a bad escape / OOM. */ +static struct st_error * +unescape_into(const char *p, const char *end, struct sbuf *sb, + struct st_span span) +{ + while (p < end) { + unsigned char c = (unsigned char)*p; + + if (c != '\\') { + if (sbuf_putc(sb, (char)c) < 0) { + return err_at_owned(span, "out of memory"); + } + p++; + continue; + } + + /* backslash */ + if (p + 1 >= end) { + return err_at_owned(span, "unterminated escape sequence in string"); + } + { + unsigned char e = (unsigned char)p[1]; + + /* whitespace escape: '\' + one-or-more whitespace/newlines */ + if (e == ' ' || e == '\t' || newline_len(p + 1, end) > 0) { + p += 2; + while (p < end) { + size_t nl = newline_len(p, end); + + if (nl > 0) { + p += nl; + } else if (*p == ' ' || *p == '\t') { + p++; + } else { + break; + } + } + continue; + } + + switch (e) { + case 'n': + if (sbuf_putc(sb, '\n') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'r': + if (sbuf_putc(sb, '\r') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 't': + if (sbuf_putc(sb, '\t') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case '\\': + if (sbuf_putc(sb, '\\') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case '"': + if (sbuf_putc(sb, '"') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case '\'': + if (sbuf_putc(sb, '\'') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'b': + if (sbuf_putc(sb, '\b') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'f': + if (sbuf_putc(sb, '\f') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'v': + if (sbuf_putc(sb, '\v') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'a': + if (sbuf_putc(sb, '\a') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 's': + if (sbuf_putc(sb, ' ') < 0) return err_at_owned(span, "out of memory"); + p += 2; + break; + case 'x': { + /* \xHH -> one byte (lenient superset of KDL) */ + int hi, lo; + unsigned char byte; + + if (end - p < 4) { + return err_at_owned(span, "invalid hex escape: expected two hex digits"); + } + hi = digit_value(p[2]); + lo = digit_value(p[3]); + if (hi < 0 || lo < 0) { + return err_at_owned(span, "invalid hex escape: expected two hex digits"); + } + byte = (unsigned char)((hi << 4) | lo); + if (byte == 0) { + return err_at_owned(span, "NUL byte is not representable in a string value"); + } + if (sbuf_putc(sb, (char)byte) < 0) return err_at_owned(span, "out of memory"); + p += 4; + break; + } + case 'u': + case 'U': { + /* \u{...} / \U{...} -> Unicode scalar value, UTF-8 encoded */ + unsigned long cp = 0; + int ndigits = 0; + + p += 2; /* skip '\u' */ + if (p >= end || *p != '{') { + return err_at_owned(span, "invalid unicode escape: expected '{'"); + } + p++; + while (p < end && is_hex_digit_c(*p)) { + cp = cp * 16 + (unsigned long)digit_value(*p); + ndigits++; + if (cp > 0x10FFFF) { + cp = 0x110000; /* force the scalar-value check below */ + } + p++; + } + if (p >= end || *p != '}') { + return err_at_owned(span, "invalid unicode escape: expected '}'"); + } + if (ndigits == 0 || ndigits > 8) { + return err_at_owned(span, "invalid unicode escape: expected 1-8 hex digits"); + } + if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + return err_at_owned(span, "invalid unicode escape: not a Unicode scalar value"); + } + p++; /* '}' */ + if (sbuf_put_utf8(sb, cp) < 0) return err_at_owned(span, "out of memory"); + break; + } + default: + return err_at_owned(span, "unknown escape sequence in string"); + } + } + } + return NULL; +} + +/* ---- multi-line strings ----------------------------------------------- */ + +/* Normalize literal newline sequences in [p, end) to a single LF, appending + * the result to sb. */ +static struct st_error * +normalize_newlines(const char *p, const char *end, struct sbuf *sb, + struct st_span span) +{ + while (p < end) { + size_t nl = newline_len(p, end); + + if (nl > 0) { + if (sbuf_putc(sb, '\n') < 0) return err_at_owned(span, "out of memory"); + p += nl; + } else { + if (sbuf_putc(sb, *p) < 0) return err_at_owned(span, "out of memory"); + p++; + } + } + return NULL; +} + +/* Emit the content lines in [s, e) (newline-separated) to out, stripping the + * common `prefix` (plen bytes) from each non-empty line. Whitespace-only + * lines always become empty lines. Returns an owned error on OOM. */ +static struct st_error * +emit_lines(struct sbuf *out, const char *s, const char *e, + const char *prefix, size_t plen, struct st_span span) +{ + const char *line = s; + bool first = true; + + if (s == e) { + return NULL; /* zero lines */ + } + for (;;) { + const char *nl = memchr(line, '\n', (size_t)(e - line)); + const char *le = nl != NULL ? nl : e; + size_t len = (size_t)(le - line); + + if (!first && sbuf_putc(out, '\n') < 0) { + return err_at_owned(span, "out of memory"); + } + first = false; + + if (!is_whitespace_only(line, le)) { + const char *q = line; + + if (plen > 0 && len >= plen && memcmp(line, prefix, plen) == 0) { + q += plen; + len -= plen; + } + if (len > 0 && sbuf_append(out, q, len) < 0) { + return err_at_owned(span, "out of memory"); + } + } + + if (nl == NULL) { + break; + } + line = nl + 1; + } + return NULL; +} + +/* Apply newline normalization + leading-newline removal + common-indent + * dedent to the multi-line body [body, body_end), appending the STILL-ESCAPED + * result to out. See the file header for the documented simplifications. */ +static struct st_error * +multiline_dedent(const char *body, const char *body_end, struct sbuf *out, + struct st_span span) +{ + struct sbuf norm = { 0 }; + struct st_error *err; + const char *p; + const char *end; + const char *last_nl = NULL; + const char *q; + + err = normalize_newlines(body, body_end, &norm, span); + if (err != NULL) { + sbuf_free(&norm); + return err; + } + + p = norm.data; + end = norm.data + norm.len; + + if (p == end) { + sbuf_free(&norm); + return NULL; /* empty */ + } + + /* Leading-newline removal (mandatory in spec; skip it when present). */ + if (*p == '\n') { + p++; + } + + for (q = p; q < end; q++) { + if (*q == '\n') { + last_nl = q; + } + } + + if (last_nl != NULL) { + const char *suffix = last_nl + 1; + + if (is_whitespace_only(suffix, end)) { + /* suffix is the closing line's whitespace = the dedent prefix */ + err = emit_lines(out, p, last_nl, suffix, (size_t)(end - suffix), + span); + } else { + /* illegal closing line: emit everything verbatim (lenient) */ + err = emit_lines(out, p, end, NULL, 0, span); + } + } else if (is_whitespace_only(p, end)) { + /* remainder is pure closing whitespace -> empty value */ + err = NULL; + } else { + /* no closing newline: whole remainder is content (lenient) */ + err = emit_lines(out, p, end, NULL, 0, span); + } + + sbuf_free(&norm); + return err; +} + +/* ---- number parsing --------------------------------------------------- */ + +/* Parse integer digits in `radix` (underscores allowed between/after digits, + * not before) from [p, end) into an overflow-checked u64. Returns NULL on + * success or an owned error. */ +static struct st_error * +parse_int_digits(const char *p, const char *end, int radix, + unsigned long long *out, struct st_span span) +{ + unsigned long long val = 0; + bool any = false; + + while (p < end) { + if (*p == '_') { + if (!any) { + return err_at_owned(span, "underscore before the first digit"); + } + p++; + continue; + } + { + int d = digit_value(*p); + + if (d < 0 || d >= radix) { + break; + } + any = true; + if (val > (UINT64_MAX - (unsigned long long)d) / + (unsigned long long)radix) { + return err_at_owned(span, "integer overflow"); + } + val = val * (unsigned long long)radix + (unsigned long long)d; + p++; + } + } + + if (!any) { + return err_at_owned(span, "invalid digit in number"); + } + while (p < end && *p == '_') { + p++; /* trailing underscores are legal */ + } + if (p != end) { + return err_at_owned(span, "invalid digit in number"); + } + *out = val; + return NULL; +} + +/* 10^n via exponentiation by squaring (no libm). */ +static double +pow10_uint(unsigned long long n) +{ + double r = 1.0; + double b = 10.0; + + while (n > 0) { + if ((n & 1ULL) != 0) { + r *= b; + } + b *= b; + n >>= 1; + } + return r; +} + +/* Parse a decimal float (integer, optional fraction, optional exponent) from + * [p, end) into *out. Underscores allowed within digit runs. */ +static struct st_error * +parse_float(const char *p, const char *end, double *out, struct st_span span) +{ + double val = 0.0; + + while (p < end) { + if (*p == '_') { + p++; + } else if (is_digit_c(*p)) { + val = val * 10.0 + (double)(*p - '0'); + p++; + } else { + break; + } + } + + if (p < end && *p == '.') { + double scale = 0.1; + + p++; + while (p < end) { + if (*p == '_') { + p++; + } else if (is_digit_c(*p)) { + val += (double)(*p - '0') * scale; + scale *= 0.1; + p++; + } else { + break; + } + } + } + + if (p < end && (*p == 'e' || *p == 'E')) { + bool eneg = false; + unsigned long long exp = 0; + bool any_exp = false; + double pw; + + p++; + if (p < end && (*p == '+' || *p == '-')) { + eneg = (*p == '-'); + p++; + } + while (p < end) { + if (*p == '_') { + p++; + } else if (is_digit_c(*p)) { + any_exp = true; + if (exp < 100000) { + exp = exp * 10 + (unsigned long long)(*p - '0'); + } + p++; + } else { + break; + } + } + if (!any_exp) { + return err_at_owned(span, "invalid exponent"); + } + pw = pow10_uint(exp); + if (eneg) { + val /= pw; + } else { + val *= pw; + } + } + + while (p < end && *p == '_') { + p++; + } + if (p != end) { + return err_at_owned(span, "invalid character in number"); + } + *out = val; + return NULL; +} + +/* How a type annotation affects the interpretation of a number literal. */ +enum ann_kind { + ANN_NONE = 0, /* no recognized numeric annotation (default) */ + ANN_UINT, /* u8/u16/u32/u64/usize -> unsigned 64-bit */ + ANN_INT, /* i8/i16/i32/i64/isize -> signed 64-bit */ + ANN_FLOAT, /* f32/f64 -> floating point */ +}; + +static enum ann_kind +classify_number_annotation(const char *s) +{ + if (s == NULL) { + return ANN_NONE; + } + if (strcmp(s, "u8") == 0 || strcmp(s, "u16") == 0 || + strcmp(s, "u32") == 0 || strcmp(s, "u64") == 0 || + strcmp(s, "usize") == 0) { + return ANN_UINT; + } + if (strcmp(s, "i8") == 0 || strcmp(s, "i16") == 0 || + strcmp(s, "i32") == 0 || strcmp(s, "i64") == 0 || + strcmp(s, "isize") == 0) { + return ANN_INT; + } + if (strcmp(s, "f32") == 0 || strcmp(s, "f64") == 0) { + return ANN_FLOAT; + } + return ANN_NONE; +} + +static struct st_error * +value_number(const struct st_kdl_token_ref *tok, struct st_kdl_value *out) +{ + const char *p = tok->text; + const char *end = tok->text + tok->len; + bool negative = false; + int radix = 10; + bool is_float = false; + enum ann_kind ann = classify_number_annotation(out->annotation); + bool force_unsigned = (ann == ANN_UINT); + bool force_float = (ann == ANN_FLOAT); + unsigned long long mag; + struct st_error *err; + + if (p < end && (*p == '+' || *p == '-')) { + negative = (*p == '-'); + p++; + } + + if (end - p >= 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + radix = 16; + p += 2; + } else if (end - p >= 2 && p[0] == '0' && (p[1] == 'o' || p[1] == 'O')) { + radix = 8; + p += 2; + } else if (end - p >= 2 && p[0] == '0' && (p[1] == 'b' || p[1] == 'B')) { + radix = 2; + p += 2; + } else { + const char *q; + + for (q = p; q < end; q++) { + if (*q == '.' || *q == 'e' || *q == 'E') { + is_float = true; + break; + } + } + } + + if (is_float || force_float) { + double val = 0.0; + + if (radix != 10) { + /* f-annotation over a radix integer: parse then widen */ + err = parse_int_digits(p, end, radix, &mag, tok->span); + if (err != NULL) { + return err; + } + val = (double)mag; + } else { + err = parse_float(p, end, &val, tok->span); + if (err != NULL) { + return err; + } + } + if (negative) { + val = -val; + } + out->kind = ST_KDL_VAL_FLOAT; + out->as.f = val; + return NULL; + } + + err = parse_int_digits(p, end, radix, &mag, tok->span); + if (err != NULL) { + return err; + } + + if (force_unsigned) { + if (negative) { + return err_at_owned(tok->span, + "negative value for an unsigned integer annotation"); + } + out->kind = ST_KDL_VAL_UINT; + out->as.u = mag; + return NULL; + } + + /* signed (default i64) */ + if (negative) { + const unsigned long long mag_max = (unsigned long long)INT64_MAX + 1ULL; + + if (mag > mag_max) { + return err_at_owned(tok->span, "integer overflow"); + } + out->as.i = (mag == mag_max) ? INT64_MIN : -(long long)mag; + } else { + if (mag > (unsigned long long)INT64_MAX) { + return err_at_owned(tok->span, "integer overflow"); + } + out->as.i = (long long)mag; + } + out->kind = ST_KDL_VAL_INT; + return NULL; +} + +/* ---- string value constructors ---------------------------------------- */ + +static struct st_error * +value_ident(const struct st_kdl_token_ref *tok, struct st_kdl_value *out) +{ + struct sbuf sb = { 0 }; + + if (sbuf_append(&sb, tok->text, tok->len) < 0) { + sbuf_free(&sb); + return err_at_owned(tok->span, "out of memory"); + } + return finish_string(&sb, out, tok->span); +} + +static struct st_error * +value_quoted(const struct st_kdl_token_ref *tok, struct st_kdl_value *out) +{ + struct sbuf sb = { 0 }; + struct st_error *err; + + if (tok->len < 2) { + return err_at_owned(tok->span, "malformed string token"); + } + err = unescape_into(tok->text + 1, tok->text + tok->len - 1, &sb, + tok->span); + if (err != NULL) { + sbuf_free(&sb); + return err; + } + return finish_string(&sb, out, tok->span); +} + +static struct st_error * +value_multiline(const struct st_kdl_token_ref *tok, struct st_kdl_value *out) +{ + struct sbuf ded = { 0 }; + struct sbuf fin = { 0 }; + struct st_error *err; + + if (tok->len < 6) { + return err_at_owned(tok->span, "malformed multi-line string token"); + } + err = multiline_dedent(tok->text + 3, tok->text + tok->len - 3, &ded, + tok->span); + if (err != NULL) { + sbuf_free(&ded); + return err; + } + err = unescape_into(ded.data, ded.data + ded.len, &fin, tok->span); + sbuf_free(&ded); + if (err != NULL) { + sbuf_free(&fin); + return err; + } + return finish_string(&fin, out, tok->span); +} + +static struct st_error * +value_raw(const struct st_kdl_token_ref *tok, struct st_kdl_value *out) +{ + const char *p = tok->text; + const char *end = tok->text + tok->len; + size_t n = 0; + bool multi; + const char *body; + const char *body_end; + struct sbuf sb = { 0 }; + + while (p < end && *p == '#') { + p++; + n++; + } + if (p >= end || *p != '"') { + return err_at_owned(tok->span, "malformed raw string token"); + } + multi = (end - p >= 3 && p[1] == '"' && p[2] == '"'); + if (multi) { + body = p + 3; + body_end = end - 3 - n; + } else { + body = p + 1; + body_end = end - 1 - n; + } + + if (multi) { + struct sbuf ded = { 0 }; + struct st_error *err = multiline_dedent(body, body_end, &ded, + tok->span); + + if (err != NULL) { + sbuf_free(&ded); + return err; + } + return finish_string(&ded, out, tok->span); + } + + if (sbuf_append(&sb, body, (size_t)(body_end - body)) < 0) { + sbuf_free(&sb); + return err_at_owned(tok->span, "out of memory"); + } + return finish_string(&sb, out, tok->span); +} + +/* ---- public API ------------------------------------------------------- */ + +struct st_error * +st_kdl_value_from_token_annotated(const struct st_kdl_token_ref *tok, + const struct st_kdl_token_ref *annotation, + struct st_kdl_value *out) +{ + struct st_kdl_value tmp; + struct st_error *err = NULL; + + memset(out, 0, sizeof(*out)); + memset(&tmp, 0, sizeof tmp); + + if (tok == NULL) { + return err_at_owned((struct st_span){ NULL, 0, 0 }, + "NULL token reference"); + } + + if (annotation != NULL) { + char *s = slice_dup(annotation->text, annotation->len); + + if (s == NULL) { + return err_at_owned(tok->span, "out of memory"); + } + if (strcmp(s, "i128") == 0 || strcmp(s, "u128") == 0) { + char msgbuf[128]; + + snprintf(msgbuf, sizeof msgbuf, + "unsupported integer width '%s': this tool supports up to " + "64-bit integers", s); + free(s); + return err_at_owned(annotation->span, msgbuf); + } + tmp.annotation = s; + } + + switch (tok->kind) { + case ST_TOK_IDENT: + err = value_ident(tok, &tmp); + break; + case ST_TOK_STRING: + err = value_quoted(tok, &tmp); + break; + case ST_TOK_MULTILINE_STRING: + err = value_multiline(tok, &tmp); + break; + case ST_TOK_RAW_STRING: + err = value_raw(tok, &tmp); + break; + case ST_TOK_NUMBER: + err = value_number(tok, &tmp); + break; + case ST_TOK_INF: + tmp.kind = ST_KDL_VAL_FLOAT; + tmp.as.f = INFINITY; + break; + case ST_TOK_NEG_INF: + tmp.kind = ST_KDL_VAL_FLOAT; + tmp.as.f = -INFINITY; + break; + case ST_TOK_NAN: + tmp.kind = ST_KDL_VAL_FLOAT; + tmp.as.f = NAN; + break; + case ST_TOK_TRUE: + tmp.kind = ST_KDL_VAL_BOOL; + tmp.as.b = true; + break; + case ST_TOK_FALSE: + tmp.kind = ST_KDL_VAL_BOOL; + tmp.as.b = false; + break; + case ST_TOK_NULL: + tmp.kind = ST_KDL_VAL_NULL; + break; + default: + err = err_at_owned(tok->span, "token is not a value"); + break; + } + + if (err != NULL) { + st_kdl_value_free(&tmp); + return err; + } + *out = tmp; + return NULL; +} + +struct st_error * +st_kdl_value_from_token(const struct st_kdl_token_ref *tok, + struct st_kdl_value *out) +{ + return st_kdl_value_from_token_annotated(tok, NULL, out); +} + +void +st_kdl_value_free(struct st_kdl_value *v) +{ + if (v == NULL) { + return; + } + if (v->kind == ST_KDL_VAL_STRING) { + free(v->as.str); + } + free(v->annotation); + v->as.str = NULL; + v->annotation = NULL; + v->kind = ST_KDL_VAL_NULL; +} + +const char * +st_kdl_value_kind_name(enum st_kdl_value_kind kind) +{ + switch (kind) { + case ST_KDL_VAL_STRING: + return "string"; + case ST_KDL_VAL_INT: + return "int"; + case ST_KDL_VAL_UINT: + return "uint"; + case ST_KDL_VAL_FLOAT: + return "float"; + case ST_KDL_VAL_BOOL: + return "bool"; + case ST_KDL_VAL_NULL: + return "null"; + default: + return "?"; + } +} diff --git a/src/kdl/value.h b/src/kdl/value.h new file mode 100644 index 0000000..3fb94b9 --- /dev/null +++ b/src/kdl/value.h @@ -0,0 +1,126 @@ +/* + * value.h - typed KDL value model for stupidtools (todo 8). + * + * The parser (todo 7) produces RAW token references (src/kdl/ast.h); this + * module INTERPRETS them into typed values. A KDL value is one of: + * + * - string (identifier, quoted, raw, or multi-line; escapes resolved) + * - integer (i64) and unsigned (u64) — numbers without a decimal point + * - float (f64) — numbers with a fraction/exponent, plus #inf/#-inf/#nan + * - bool (#true / #false) + * - null (#null) + * + * plus an optional type-annotation string (KDL §3.8), e.g. the `u8` in + * `(u8)42`, captured into `annotation` (an owned NUL-terminated copy, or + * NULL when the value was not annotated). + * + * SUBSET (documented deviation from KDL 2.0.0) + * -------------------------------------------- + * KDL reserves the integer-width annotations i8/i16/i32/i64/isize and + * u8/u16/u32/u64/usize (plus i128/u128) and the float annotations f32/f64. + * This tool's value model supports at most 64-bit integers: an `i128` or + * `u128` annotation yields a clear "unsupported width" error + * (ST_ERR_KDL_PARSE) instead of a value. The `u*` annotations select the + * unsigned (u64) interpretation; `i*` the signed (i64) one; `f32`/`f64` + * the float interpretation. Any other annotation is captured verbatim and + * leaves the literal's natural interpretation unchanged (the DSL schema, + * todo 9, is responsible for rejecting application-invalid annotations). + * + * NUMBER INTERPRETATION (KDL §3.14) + * --------------------------------- + * Radix prefixes 0x/0o/0b (case-insensitive) and decimal; underscores + * allowed between/after digits; optional leading +/-. A literal with a + * decimal point or exponent is a float; otherwise an integer. Integers + * overflow-check to i64 (default) or u64 (u*-annotated) and produce a + * spanned error on overflow. #inf/#-inf/#nan map to INFINITY/-INFINITY/NAN + * (math.h macros; no libm linkage is required). + * + * STRING UNESCAPING (KDL §3.9/3.11/3.12) + * -------------------------------------- + * Quoted and multi-line strings resolve the KDL escapes (\n \r \t \\ \" \b + * \f \s \u{...}) plus the lenient extras \' \a \v \xHH \U{...} (harmless + * superset; the lexer already rejects non-KDL escapes, so these only matter + * for directly-constructed token refs). A backslash followed by literal + * whitespace/newline is a whitespace escape and vanishes. Unknown escapes, + * a lone trailing backslash, and non-scalar-value unicode escapes are + * spanned errors. Multi-line strings additionally remove the leading + * newline and dedent by the closing line's whitespace (see value.c for the + * exact rule and its documented simplifications). `\0` is REJECTED: KDL + * forbids U+0000 and our value is a NUL-terminated C string, so a NUL byte + * cannot be represented. + * + * OWNERSHIP + * --------- + * st_kdl_value_from_token() writes into a caller-provided `struct + * st_kdl_value`; the string payload and the annotation are heap-allocated + * by this module and released by st_kdl_value_free() (NULL is a safe no-op; + * it also frees nothing for non-string kinds). The token ref's text slice + * remains borrowed. + * + * ERRORS + * ------ + * All failures return an owned st_error of category ST_ERR_KDL_PARSE whose + * span is heap-allocated in the same block as the error (the parser.c + * pattern), so err->span stays valid until st_error_free(err). On error the + * output value is left zeroed (kind ST_KDL_VAL_NULL) and nothing leaks. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef ST_KDL_VALUE_H +#define ST_KDL_VALUE_H + +#include +#include + +#include "ast.h" /* struct st_kdl_token_ref */ + +struct st_error; + +/* The tagged kinds a KDL value can take. */ +enum st_kdl_value_kind { + ST_KDL_VAL_STRING = 0, /* as.str (owned, NUL-terminated) */ + ST_KDL_VAL_INT, /* as.i (i64) */ + ST_KDL_VAL_UINT, /* as.u (u64) */ + ST_KDL_VAL_FLOAT, /* as.f (f64) */ + ST_KDL_VAL_BOOL, /* as.b */ + ST_KDL_VAL_NULL, /* no payload */ +}; + +/* A typed KDL value. `annotation` is an owned NUL-terminated copy of the + * type-annotation identifier, or NULL when the value was not annotated. */ +struct st_kdl_value { + enum st_kdl_value_kind kind; + union { + char *str; /* STRING: owned, NUL-terminated */ + long long i; /* INT */ + unsigned long long u; /* UINT */ + double f; /* FLOAT */ + bool b; /* BOOL */ + } as; + char *annotation; /* owned; may be NULL */ +}; + +/* Interpret a raw value token ref into `out` (no type annotation). On + * success returns NULL and `out` is fully populated; on failure returns an + * owned error and `out` is left zeroed. */ +struct st_error *st_kdl_value_from_token(const struct st_kdl_token_ref *tok, + struct st_kdl_value *out); + +/* Same, but with an explicit type-annotation ref (ST_TOK_TYPE) captured + * into `out->annotation`. `annotation` may be NULL. */ +struct st_error *st_kdl_value_from_token_annotated( + const struct st_kdl_token_ref *tok, + const struct st_kdl_token_ref *annotation, + struct st_kdl_value *out); + +/* Release the payloads owned by `v` (string + annotation). NULL is a safe + * no-op. */ +void st_kdl_value_free(struct st_kdl_value *v); + +/* Stable display name for a value kind, e.g. "string". Unknown kinds yield + * "?" rather than indexing out of range. */ +const char *st_kdl_value_kind_name(enum st_kdl_value_kind kind); + +#endif /* ST_KDL_VALUE_H */ diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c new file mode 100644 index 0000000..c3c71ba --- /dev/null +++ b/tests/unit/test_value.c @@ -0,0 +1,632 @@ +/* LINK: ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */ +/* tests/unit/test_value.c + * + * Unit tests for the typed KDL value model (todo 8). + * + * The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists the + * extra .c sources to compile into this test binary (paths relative to + * tests/unit/, space-separated). value.c depends on error.c (typed errors) and + * span.c (caret rendering); parser.c + lexer.c are linked so the value-level + * type-annotation integration can be tested END-TO-END through the parser. + * + * Most value-model cases are exercised by constructing a token ref DIRECTLY + * (mkref), which bypasses the lexer's already-strict escape/radix validation + * and lets us probe the value model's own boundary handling (unknown escapes, + * `0xZZ`, overflow, i128 width) that the lexer would otherwise reject first. + * Assertions check CONCRETE parsed values (kind + payload + annotation), never + * just "does not crash". + */ +#include "munit.h" + +#include "error.h" +#include "kdl/ast.h" +#include "kdl/lexer.h" +#include "kdl/value.h" + +#include +#include +#include +#include + +/* ---- token-ref construction ------------------------------------------- */ + +/* Build a borrowed token ref over a NUL-terminated C string (the string + * literal outlives the value call, so the borrow is safe). */ +static struct st_kdl_token_ref +mkref(enum st_token_kind kind, const char *text) +{ + struct st_kdl_token_ref r; + + r.kind = kind; + r.span.file = "test.kdl"; + r.span.line = 1; + r.span.col = 1; + r.text = text; + r.len = strlen(text); + return r; +} + +/* Parse a value token directly (no annotation); assert success and return + * the value. munit's longjmp aborts the test on assertion failure. */ +static struct st_kdl_value +ok_value(enum st_token_kind kind, const char *text) +{ + struct st_kdl_token_ref r = mkref(kind, text); + struct st_kdl_value v; + struct st_error *err = st_kdl_value_from_token(&r, &v); + + munit_assert_null(err); + return v; +} + +/* Parse a value token with an explicit type-annotation ref; assert success. */ +static struct st_kdl_value +ok_annotated(enum st_token_kind kind, const char *text, const char *ann) +{ + struct st_kdl_token_ref r = mkref(kind, text); + struct st_kdl_token_ref a = mkref(ST_TOK_TYPE, ann); + struct st_kdl_value v; + struct st_error *err = st_kdl_value_from_token_annotated(&r, &a, &v); + + munit_assert_null(err); + return v; +} + +/* Assert that parsing `text` (kind) fails with category KDL_PARSE and a + * message containing `needle`. */ +static void +expect_error(enum st_token_kind kind, const char *text, const char *needle) +{ + struct st_kdl_token_ref r = mkref(kind, text); + struct st_kdl_value v; + struct st_error *err = st_kdl_value_from_token(&r, &v); + + munit_assert_not_null(err); + munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); + munit_assert_not_null(err->span); + munit_assert_true(strstr(st_error_message(err), needle) != NULL); + st_error_free(err); +} + +/* ---- number tests ------------------------------------------------------ */ + +static MunitResult +test_radix_ints(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_NUMBER, "0xff"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 255); + + v = ok_value(ST_TOK_NUMBER, "0o17"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 15); + + v = ok_value(ST_TOK_NUMBER, "0b101"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 5); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_decimal_and_sign(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_NUMBER, "42"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 42); + + v = ok_value(ST_TOK_NUMBER, "-42"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, -42); + + v = ok_value(ST_TOK_NUMBER, "+7"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 7); + + v = ok_value(ST_TOK_NUMBER, "1_000"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_INT); + munit_assert_llong(v.as.i, ==, 1000); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_floats(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_NUMBER, "1.5"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_double_equal(v.as.f, 1.5, 12); + + /* trailing dot, no fraction digits (direct ref: the lexer splits this) */ + v = ok_value(ST_TOK_NUMBER, "3."); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_double_equal(v.as.f, 3.0, 12); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_exponents(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_NUMBER, "1e10"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_double_equal(v.as.f, 1e10, 12); + + v = ok_value(ST_TOK_NUMBER, "2.5e-3"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_double_equal(v.as.f, 2.5e-3, 12); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_keyword_numbers(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_INF, "#inf"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_true(isinf(v.as.f)); + munit_assert_true(v.as.f > 0); + + v = ok_value(ST_TOK_NEG_INF, "#-inf"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_true(isinf(v.as.f)); + munit_assert_true(v.as.f < 0); + + v = ok_value(ST_TOK_NAN, "#nan"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_FLOAT); + munit_assert_true(isnan(v.as.f)); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_bool_null(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_TRUE, "#true"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_BOOL); + munit_assert_true(v.as.b); + + v = ok_value(ST_TOK_FALSE, "#false"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_BOOL); + munit_assert_false(v.as.b); + + v = ok_value(ST_TOK_NULL, "#null"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_NULL); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +/* ---- string tests ------------------------------------------------------ */ + +static MunitResult +test_ident_and_quoted(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + v = ok_value(ST_TOK_IDENT, "foo"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "foo"); + st_kdl_value_free(&v); + + v = ok_value(ST_TOK_STRING, "\"hello\""); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "hello"); + st_kdl_value_free(&v); + + /* empty quoted string */ + v = ok_value(ST_TOK_STRING, "\"\""); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, ""); + st_kdl_value_free(&v); + + return MUNIT_OK; +} + +static MunitResult +test_escapes(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + /* "a\nb" -> a, newline, b */ + v = ok_value(ST_TOK_STRING, "\"a\\nb\""); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "a\nb"); + st_kdl_value_free(&v); + + /* backslash + quote + tab + carriage return */ + v = ok_value(ST_TOK_STRING, "\"\\\\\\\"\\t\\r\""); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "\\\"\t\r"); + st_kdl_value_free(&v); + + /* space escape \s */ + v = ok_value(ST_TOK_STRING, "\"a\\sb\""); + munit_assert_string_equal(v.as.str, "a b"); + st_kdl_value_free(&v); + + /* backspace + form feed + vertical tab + bell + apostrophe (lenient) */ + v = ok_value(ST_TOK_STRING, "\"\\b\\f\\v\\a\\'\""); + munit_assert_string_equal(v.as.str, "\b\f\v\a'"); + st_kdl_value_free(&v); + + /* hex byte \x41 -> 'A' */ + v = ok_value(ST_TOK_STRING, "\"\\x41\""); + munit_assert_string_equal(v.as.str, "A"); + st_kdl_value_free(&v); + + /* unicode \u{1F600} -> U+1F600 (UTF-8 F0 9F 98 80) */ + v = ok_value(ST_TOK_STRING, "\"\\u{1F600}\""); + munit_assert_string_equal(v.as.str, "\xF0\x9F\x98\x80"); + st_kdl_value_free(&v); + + /* \u{10FFFF} is the maximum Unicode scalar value (UTF-8 F4 8F BF BF) */ + v = ok_value(ST_TOK_STRING, "\"\\u{10FFFF}\""); + munit_assert_string_equal(v.as.str, "\xF4\x8F\xBF\xBF"); + st_kdl_value_free(&v); + + /* whitespace escape: backslash + spaces + newline -> nothing */ + v = ok_value(ST_TOK_STRING, "\"Hello \\ World\""); + munit_assert_string_equal(v.as.str, "Hello World"); + st_kdl_value_free(&v); + + return MUNIT_OK; +} + +static MunitResult +test_raw_string(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + + /* #"raw\n"# -> literal backslash-n (no escape processing) */ + v = ok_value(ST_TOK_RAW_STRING, "#\"raw\\n\"#"); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "raw\\n"); + st_kdl_value_free(&v); + + return MUNIT_OK; +} + +static MunitResult +test_multiline_dedent(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + const char *ml = "\"\"\"\n foo\n bar\n \"\"\""; + + v = ok_value(ST_TOK_MULTILINE_STRING, ml); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "foo\nbar"); + st_kdl_value_free(&v); + + /* no trailing newline issues: an empty multi-line string */ + v = ok_value(ST_TOK_MULTILINE_STRING, "\"\"\"\n \"\"\""); + munit_assert_string_equal(v.as.str, ""); + st_kdl_value_free(&v); + + return MUNIT_OK; +} + +static MunitResult +test_multiline_escaped_newline(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v; + /* interior \n escape stays a newline (not dedented away) */ + const char *ml = "\"\"\"\n a\\nb\n \"\"\""; + + v = ok_value(ST_TOK_MULTILINE_STRING, ml); + munit_assert_int(v.kind, ==, ST_KDL_VAL_STRING); + munit_assert_string_equal(v.as.str, "a\nb"); + st_kdl_value_free(&v); + + return MUNIT_OK; +} + +/* ---- error tests ------------------------------------------------------- */ + +static MunitResult +test_unknown_escape(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + /* '\q' is not a KDL escape (direct ref; the lexer would reject it) */ + expect_error(ST_TOK_STRING, "\"a\\qb\"", "unknown escape"); + return MUNIT_OK; +} + +static MunitResult +test_unterminated_escape(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + /* lone trailing backslash inside the quotes ("abc\" -> body "abc\") */ + expect_error(ST_TOK_STRING, "\"abc\\\"", "unterminated"); + return MUNIT_OK; +} + +static MunitResult +test_bad_radix(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + expect_error(ST_TOK_NUMBER, "0xZZ", "invalid digit"); + expect_error(ST_TOK_NUMBER, "0o8", "invalid digit"); + expect_error(ST_TOK_NUMBER, "0b2", "invalid digit"); + return MUNIT_OK; +} + +static MunitResult +test_integer_overflow(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + /* 24 digits, far beyond u64 (and i64) */ + expect_error(ST_TOK_NUMBER, "999999999999999999999999", "overflow"); + + /* 2^63 as an unannotated (signed) literal is one past i64 max */ + expect_error(ST_TOK_NUMBER, "9223372036854775808", "overflow"); + return MUNIT_OK; +} + +/* ---- annotation tests -------------------------------------------------- */ + +static MunitResult +test_unicode_out_of_range(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + /* above the scalar-value range */ + expect_error(ST_TOK_STRING, "\"\\u{110000}\"", "Unicode scalar"); + /* a UTF-16 surrogate is not a scalar value */ + expect_error(ST_TOK_STRING, "\"\\u{D800}\"", "Unicode scalar"); + return MUNIT_OK; +} + +static MunitResult +test_annotation_u8(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_value v = ok_annotated(ST_TOK_NUMBER, "42", "u8"); + + munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); + munit_assert_ullong(v.as.u, ==, 42); + munit_assert_not_null(v.annotation); + munit_assert_string_equal(v.annotation, "u8"); + + st_kdl_value_free(&v); + return MUNIT_OK; +} + +static MunitResult +test_annotation_unsigned_rejects_negative(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_token_ref r = mkref(ST_TOK_NUMBER, "-1"); + struct st_kdl_token_ref a = mkref(ST_TOK_TYPE, "u8"); + struct st_kdl_value v; + struct st_error *err = st_kdl_value_from_token_annotated(&r, &a, &v); + + munit_assert_not_null(err); + munit_assert_true(strstr(st_error_message(err), "unsigned") != NULL); + st_error_free(err); + return MUNIT_OK; +} + +static MunitResult +test_annotation_unsupported_width(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_kdl_token_ref r = mkref(ST_TOK_NUMBER, "42"); + struct st_kdl_token_ref a128 = mkref(ST_TOK_TYPE, "i128"); + struct st_kdl_token_ref u128 = mkref(ST_TOK_TYPE, "u128"); + struct st_kdl_value v; + struct st_error *err; + + err = st_kdl_value_from_token_annotated(&r, &a128, &v); + munit_assert_not_null(err); + munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE); + munit_assert_true(strstr(st_error_message(err), "unsupported") != NULL); + st_error_free(err); + + err = st_kdl_value_from_token_annotated(&r, &u128, &v); + munit_assert_not_null(err); + munit_assert_true(strstr(st_error_message(err), "unsupported") != NULL); + st_error_free(err); + return MUNIT_OK; +} + +/* ---- end-to-end annotation integration (through the parser) ------------ */ + +static MunitResult +test_parser_annotation_arg(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_error *err = NULL; + struct st_kdl_document *doc = st_kdl_parse("node (u8)42", "t.kdl", &err); + struct st_kdl_node *n; + struct st_kdl_value v; + + munit_assert_not_null(doc); + munit_assert_null(err); + + n = doc->nodes; + munit_assert_not_null(n); + munit_assert_not_null(n->args); + munit_assert_int(n->args->value.kind, ==, ST_TOK_NUMBER); + munit_assert_not_null(n->args->annotation); + munit_assert_int(n->args->annotation->kind, ==, ST_TOK_TYPE); + munit_assert_memory_equal(2, n->args->annotation->text, "u8"); + + err = st_kdl_value_from_token_annotated(&n->args->value, + n->args->annotation, &v); + munit_assert_null(err); + munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); + munit_assert_ullong(v.as.u, ==, 42); + munit_assert_string_equal(v.annotation, "u8"); + st_kdl_value_free(&v); + + st_kdl_document_free(doc); + return MUNIT_OK; +} + +static MunitResult +test_parser_annotation_prop(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + struct st_error *err = NULL; + struct st_kdl_document *doc = st_kdl_parse("node key=(u8)42", "t.kdl", &err); + struct st_kdl_node *n; + struct st_kdl_value v; + + munit_assert_not_null(doc); + munit_assert_null(err); + + n = doc->nodes; + munit_assert_not_null(n); + munit_assert_not_null(n->props); + munit_assert_int(n->props->value.kind, ==, ST_TOK_NUMBER); + munit_assert_not_null(n->props->annotation); + munit_assert_int(n->props->annotation->kind, ==, ST_TOK_TYPE); + munit_assert_memory_equal(2, n->props->annotation->text, "u8"); + + err = st_kdl_value_from_token_annotated(&n->props->value, + n->props->annotation, &v); + munit_assert_null(err); + munit_assert_int(v.kind, ==, ST_KDL_VAL_UINT); + munit_assert_ullong(v.as.u, ==, 42); + st_kdl_value_free(&v); + + st_kdl_document_free(doc); + return MUNIT_OK; +} + +static MunitResult +test_free_null(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + st_kdl_value_free(NULL); + return MUNIT_OK; +} + +static MunitResult +test_kind_name(const MunitParameter params[], void *data) +{ + (void)params; + (void)data; + + munit_assert_string_equal(st_kdl_value_kind_name(ST_KDL_VAL_STRING), + "string"); + munit_assert_string_equal(st_kdl_value_kind_name(ST_KDL_VAL_NULL), "null"); + munit_assert_string_equal(st_kdl_value_kind_name((enum st_kdl_value_kind)99), + "?"); + return MUNIT_OK; +} + +static MunitTest tests[] = { + { "/value/radix-ints", test_radix_ints, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/decimal-and-sign", test_decimal_and_sign, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/floats", test_floats, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/exponents", test_exponents, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/keyword-numbers", test_keyword_numbers, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/bool-null", test_bool_null, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/ident-and-quoted", test_ident_and_quoted, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/escapes", test_escapes, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/raw-string", test_raw_string, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/multiline-dedent", test_multiline_dedent, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/multiline-escaped-newline", test_multiline_escaped_newline, NULL, + NULL, MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/unknown-escape", test_unknown_escape, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/unterminated-escape", test_unterminated_escape, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/unicode-out-of-range", test_unicode_out_of_range, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/bad-radix", test_bad_radix, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/integer-overflow", test_integer_overflow, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/annotation-u8", test_annotation_u8, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/annotation-unsigned-negative", test_annotation_unsigned_rejects_negative, + NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/annotation-unsupported-width", test_annotation_unsupported_width, + NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/parser-annotation-arg", test_parser_annotation_arg, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/parser-annotation-prop", test_parser_annotation_prop, NULL, NULL, + MUNIT_TEST_OPTION_NONE, NULL }, + { "/value/free-null", test_free_null, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { "/value/kind-name", test_kind_name, NULL, NULL, MUNIT_TEST_OPTION_NONE, + NULL }, + { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, +}; + +static const MunitSuite suite = { + "/value", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE, +}; + +int +main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)]) +{ + return munit_suite_main(&suite, NULL, argc, argv); +}