feat(kdl): add typed KDL value model

This commit is contained in:
2026-08-28 21:18:16 -04:00
parent 783019dff4
commit 467d10bd0c
5 changed files with 1908 additions and 16 deletions
+5 -1
View File
@@ -56,14 +56,18 @@ struct st_kdl_token_ref {
/* A positional argument, in source order (spec mandates order). */ /* A positional argument, in source order (spec mandates order). */
struct st_kdl_arg { struct st_kdl_arg {
struct st_kdl_token_ref value; struct st_kdl_token_ref value;
struct st_kdl_token_ref *annotation; /* NULL when not annotated */
struct st_kdl_arg *next; struct st_kdl_arg *next;
}; };
/* A `key = value` property, in source order. The key is a string-ish /* 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_prop {
struct st_kdl_token_ref key; struct st_kdl_token_ref key;
struct st_kdl_token_ref value; struct st_kdl_token_ref value;
struct st_kdl_token_ref *annotation; /* NULL when not annotated */
struct st_kdl_prop *next; struct st_kdl_prop *next;
}; };
+85 -8
View File
@@ -218,7 +218,7 @@ ref_from_token(struct st_kdl_token_ref *ref, const struct st_token *t)
} }
static struct st_kdl_arg * 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)); struct st_kdl_arg *a = calloc(1, sizeof(*a));
@@ -226,11 +226,20 @@ arg_new(const struct st_token *t)
return NULL; return NULL;
} }
ref_from_token(&a->value, t); 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; return a;
} }
static struct st_kdl_prop * 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)); 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->key, key);
ref_from_token(&pr->value, value); 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; return pr;
} }
@@ -278,11 +295,13 @@ node_free(struct st_kdl_node *n)
free(n->type); free(n->type);
for (a = n->args; a != NULL;) { for (a = n->args; a != NULL;) {
struct st_kdl_arg *nx = a->next; struct st_kdl_arg *nx = a->next;
free(a->annotation);
free(a); free(a);
a = nx; a = nx;
} }
for (pr = n->props; pr != NULL;) { for (pr = n->props; pr != NULL;) {
struct st_kdl_prop *px = pr->next; struct st_kdl_prop *px = pr->next;
free(pr->annotation);
free(pr); free(pr);
pr = px; pr = px;
} }
@@ -444,12 +463,45 @@ parse_node(struct st_parser *p, struct st_error **err)
*err = err_at_owned(p->tok.span, "unexpected '='"); *err = err_at_owned(p->tok.span, "unexpected '='");
node_free(n); node_free(n);
return NULL; return NULL;
case ST_TOK_TYPE: case ST_TOK_TYPE: {
*err = err_at_owned(p->tok.span, /* Value annotation: capture it, then require an argument value.
"unexpected type annotation (value annotations are " * (A type annotation in entry position prefixes a VALUE, so the
"not supported yet)"); * 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); node_free(n);
return NULL; 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: default:
if (!tok_is_value(k)) { if (!tok_is_value(k)) {
*err = err_at_owned(p->tok.span, *err = err_at_owned(p->tok.span,
@@ -491,13 +543,38 @@ parse_node(struct st_parser *p, struct st_error **err)
node_free(n); node_free(n);
return NULL; 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)) { if (!tok_is_value(k)) {
*err = err_at_owned(p->tok.span, *err = err_at_owned(p->tok.span,
"expected a value after '='"); "expected a value after '='");
node_free(n); node_free(n);
return NULL; return NULL;
} }
pr = prop_new(&val_tok, &p->tok); pr = prop_new(&val_tok, &p->tok, &vann_tok, has_vann);
}
if (pr == NULL) { if (pr == NULL) {
*err = err_at_owned(val_tok.span, "out of memory"); *err = err_at_owned(val_tok.span, "out of memory");
node_free(n); node_free(n);
@@ -516,7 +593,7 @@ parse_node(struct st_parser *p, struct st_error **err)
} }
/* Argument. */ /* Argument. */
{ {
struct st_kdl_arg *a = arg_new(&val_tok); struct st_kdl_arg *a = arg_new(&val_tok, NULL, false);
if (a == NULL) { if (a == NULL) {
*err = err_at_owned(val_tok.span, "out of memory"); *err = err_at_owned(val_tok.span, "out of memory");
+1053
View File
File diff suppressed because it is too large Load Diff
+126
View File
@@ -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 <stdbool.h>
#include <stddef.h>
#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 */
+632
View File
@@ -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 <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
/* ---- 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);
}