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). */
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;
};
+92 -15
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 *
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");
+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 */