Template
feat(kdl): add KDL parser producing AST
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* ast.h - KDL 2.0.0 syntax tree for stupidtools (todo 7).
|
||||
*
|
||||
* The tree mirrors the KDL node grammar:
|
||||
*
|
||||
* document := node*
|
||||
* node := [type-annotation] name (arg | prop)* children?
|
||||
*
|
||||
* Every element is a RAW TOKEN REFERENCE into the original source buffer:
|
||||
* kind + span + text slice. Strings keep their delimiters and escapes,
|
||||
* numbers keep their radix spelling, keywords keep their `#` spelling.
|
||||
* INTERPRETATION (unescaping, radix conversion, keyword-to-value mapping,
|
||||
* value-level type annotations) is the typed value model (todo 8) and
|
||||
* deliberately does NOT happen here.
|
||||
*
|
||||
* OWNERSHIP
|
||||
* ---------
|
||||
* st_kdl_parse() allocates every node/arg/prop on the heap;
|
||||
* st_kdl_document_free() releases the whole tree (NULL is a safe no-op).
|
||||
* Token text slices and span `file` pointers are BORROWED from the
|
||||
* caller's `src`/`filename` buffers, which must therefore outlive the
|
||||
* document.
|
||||
*
|
||||
* ERROR-SPAN OWNERSHIP (unlike the lexer's borrowed span)
|
||||
* ---------
|
||||
* The lexer borrows its error span from the lexer object (src/kdl/lexer.h).
|
||||
* The parser cannot do that: its context dies before st_kdl_parse()
|
||||
* returns. Parse errors therefore carry a span that is heap-allocated IN
|
||||
* THE SAME BLOCK as the error itself (error struct first, span struct
|
||||
* immediately after it, aligned). st_error_free() frees the block, so the
|
||||
* span needs no separate cleanup and is never dangling. Callers may read
|
||||
* err->span until st_error_free(err), exactly like err->message.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_KDL_AST_H
|
||||
#define ST_KDL_AST_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "lexer.h" /* enum st_token_kind */
|
||||
|
||||
struct st_error;
|
||||
|
||||
/* One raw token reference. `text` is a borrowed slice into the source
|
||||
* buffer; it is NOT NUL-terminated (use `len`). */
|
||||
struct st_kdl_token_ref {
|
||||
enum st_token_kind kind;
|
||||
struct st_span span;
|
||||
const char *text;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
/* A positional argument, in source order (spec mandates order). */
|
||||
struct st_kdl_arg {
|
||||
struct st_kdl_token_ref value;
|
||||
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. */
|
||||
struct st_kdl_prop {
|
||||
struct st_kdl_token_ref key;
|
||||
struct st_kdl_token_ref value;
|
||||
struct st_kdl_prop *next;
|
||||
};
|
||||
|
||||
/* A node: optional type annotation, name, ordered arguments, ordered
|
||||
* properties (interleaved in source, collapsed into the two lists while
|
||||
* preserving relative order within each), and an optional children
|
||||
* block. `next` links siblings in document order. */
|
||||
struct st_kdl_node {
|
||||
struct st_kdl_token_ref *type; /* NULL when not annotated */
|
||||
struct st_kdl_token_ref name;
|
||||
struct st_kdl_arg *args;
|
||||
struct st_kdl_prop *props;
|
||||
struct st_kdl_node *children;
|
||||
struct st_kdl_node *next;
|
||||
};
|
||||
|
||||
/* A document: zero or more top-level nodes in source order. */
|
||||
struct st_kdl_document {
|
||||
struct st_kdl_node *nodes;
|
||||
};
|
||||
|
||||
/* Parse `src` (a NUL-terminated buffer, borrowed) into an AST. On success
|
||||
* returns the document — possibly with zero nodes — and leaves `*err`
|
||||
* untouched. On failure returns NULL and, if `err != NULL`, sets `*err`
|
||||
* to an owned st_error of category ST_ERR_KDL_PARSE with a stable span
|
||||
* (see the header comment). `filename` is borrowed into every span. */
|
||||
struct st_kdl_document *st_kdl_parse(const char *src, const char *filename,
|
||||
struct st_error **err);
|
||||
|
||||
/* Release a document produced by st_kdl_parse. NULL is a safe no-op. */
|
||||
void st_kdl_document_free(struct st_kdl_document *doc);
|
||||
|
||||
#endif /* ST_KDL_AST_H */
|
||||
@@ -0,0 +1,621 @@
|
||||
/*
|
||||
* parser.c - KDL 2.0.0 recursive-descent parser for stupidtools (todo 7).
|
||||
*
|
||||
* Consumes the lexer's flat token stream (src/kdl/lexer.h) and builds the
|
||||
* AST from src/kdl/ast.h. Roles are assigned BY POSITION, per the lexer's
|
||||
* contract: a string-ish token followed by ST_TOK_EQUALS (on the same
|
||||
* line) is a property key, otherwise a bare value is an argument.
|
||||
*
|
||||
* Grammar implemented (see the plan, todo 7):
|
||||
*
|
||||
* document := node* (separated by whitespace/`;`; comments and
|
||||
* `/-` are trivia, skipped)
|
||||
* node := [type] name (arg | prop)* children?
|
||||
* children := '{' node* '}' (`;`-separated, recursively)
|
||||
* arg := value token
|
||||
* prop := string-ish-token '=' value token
|
||||
*
|
||||
* Node termination: a node ends at `;`, `/-`, the end of its parent's
|
||||
* child block (`}`), EOF, or a token on a LATER source line than the last
|
||||
* consumed token (the lexer discards newlines, so the parser infers
|
||||
* newline termination from token spans). Entries and the children block
|
||||
* may span lines only via the lexer's line continuations (esclines), which
|
||||
* the lexer consumes invisibly.
|
||||
*
|
||||
* ERRORS AND SPANS
|
||||
* ----------------
|
||||
* Every parse failure returns a st_error (ST_ERR_KDL_PARSE) carrying a
|
||||
* span. Because st_error only holds a BORROWED span pointer (src/error.h
|
||||
* is frozen), and the lexer's err_span dies with the lexer, the parser
|
||||
* copies the span into storage that is heap-allocated IN THE SAME BLOCK
|
||||
* as the error struct itself — st_error_free() releases both, so the span
|
||||
* is never dangling and never leaked. Lexer errors are likewise copied
|
||||
* into an owned block before the lexer is freed.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "kdl/ast.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "kdl/lexer.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- parser state ----------------------------------------------------- */
|
||||
|
||||
struct st_parser {
|
||||
struct st_lexer *lx;
|
||||
struct st_token tok; /* one-token lookahead */
|
||||
bool has_tok; /* is `tok` a valid peeked token? */
|
||||
};
|
||||
|
||||
/* ---- 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). 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;
|
||||
}
|
||||
|
||||
/* ---- token plumbing --------------------------------------------------- */
|
||||
|
||||
/* Pull the next token from the lexer into p->tok. On a lex error, copies
|
||||
* the error (message + span value) into an owned block, frees the
|
||||
* lexer's error, and returns ST_TOK_ERROR with *err set. */
|
||||
static enum st_token_kind
|
||||
p_fetch(struct st_parser *p, struct st_error **err)
|
||||
{
|
||||
struct st_error *lexerr = NULL;
|
||||
enum st_token_kind k;
|
||||
struct st_span sp;
|
||||
|
||||
k = st_lexer_next(p->lx, &p->tok, &lexerr);
|
||||
if (k != ST_TOK_ERROR) {
|
||||
return k;
|
||||
}
|
||||
/* Duplicate the message while `lexerr` is still alive (its span value
|
||||
* is copied by value; the message must be copied BEFORE the free). */
|
||||
if (lexerr != NULL) {
|
||||
sp = lexerr->span != NULL ? *lexerr->span : p->tok.span;
|
||||
*err = err_at_owned(sp, st_error_message(lexerr));
|
||||
st_error_free(lexerr);
|
||||
} else {
|
||||
*err = err_at_owned(p->tok.span, "lexer error");
|
||||
}
|
||||
return ST_TOK_ERROR;
|
||||
}
|
||||
|
||||
/* Peek the next token (leaving it in the lookahead). ST_TOK_ERROR on a
|
||||
* lex failure (*err set). */
|
||||
static enum st_token_kind
|
||||
p_peek(struct st_parser *p, struct st_error **err)
|
||||
{
|
||||
if (!p->has_tok) {
|
||||
enum st_token_kind k = p_fetch(p, err);
|
||||
|
||||
if (k == ST_TOK_ERROR) {
|
||||
return ST_TOK_ERROR;
|
||||
}
|
||||
p->has_tok = true;
|
||||
}
|
||||
return p->tok.kind;
|
||||
}
|
||||
|
||||
/* Discard the peeked token. */
|
||||
static void
|
||||
p_consume(struct st_parser *p)
|
||||
{
|
||||
p->has_tok = false;
|
||||
}
|
||||
|
||||
/* Skip separators/trivia at node-boundary level (document and children
|
||||
* block): line/block comments, slashdash, semicolons. Returns 0 on
|
||||
* success, -1 on lex error (*err set). */
|
||||
static int
|
||||
skip_trivia(struct st_parser *p, struct st_error **err)
|
||||
{
|
||||
for (;;) {
|
||||
enum st_token_kind k = p_peek(p, err);
|
||||
|
||||
if (k == ST_TOK_ERROR) {
|
||||
return -1;
|
||||
}
|
||||
switch (k) {
|
||||
case ST_TOK_LINE_COMMENT:
|
||||
case ST_TOK_BLOCK_COMMENT:
|
||||
case ST_TOK_SLASHDASH:
|
||||
case ST_TOK_SEMICOLON:
|
||||
p_consume(p);
|
||||
continue;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Skip comments only (positions INSIDE a node, where `;` and `/-`
|
||||
* terminate the node instead of being trivia). Returns 0 on success,
|
||||
* -1 on lex error (*err set). */
|
||||
static int
|
||||
skip_comments(struct st_parser *p, struct st_error **err)
|
||||
{
|
||||
for (;;) {
|
||||
enum st_token_kind k = p_peek(p, err);
|
||||
|
||||
if (k == ST_TOK_ERROR) {
|
||||
return -1;
|
||||
}
|
||||
if (k == ST_TOK_LINE_COMMENT || k == ST_TOK_BLOCK_COMMENT) {
|
||||
p_consume(p);
|
||||
continue;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- token classification --------------------------------------------- */
|
||||
|
||||
/* String-ish tokens: legal node names and property keys (the KDL grammar
|
||||
* "string": identifiers plus every quoted/raw/multi-line string form). */
|
||||
static bool
|
||||
tok_is_stringish(enum st_token_kind k)
|
||||
{
|
||||
return k == ST_TOK_IDENT || k == ST_TOK_STRING ||
|
||||
k == ST_TOK_MULTILINE_STRING || k == ST_TOK_RAW_STRING;
|
||||
}
|
||||
|
||||
/* Value tokens: anything usable as an argument or a property value. */
|
||||
static bool
|
||||
tok_is_value(enum st_token_kind k)
|
||||
{
|
||||
return tok_is_stringish(k) || k == ST_TOK_NUMBER || k == ST_TOK_INF ||
|
||||
k == ST_TOK_NEG_INF || k == ST_TOK_NAN || k == ST_TOK_TRUE ||
|
||||
k == ST_TOK_FALSE || k == ST_TOK_NULL;
|
||||
}
|
||||
|
||||
/* ---- AST construction ------------------------------------------------- */
|
||||
|
||||
static void
|
||||
ref_from_token(struct st_kdl_token_ref *ref, const struct st_token *t)
|
||||
{
|
||||
ref->kind = t->kind;
|
||||
ref->span = t->span;
|
||||
ref->text = t->text;
|
||||
ref->len = t->len;
|
||||
}
|
||||
|
||||
static struct st_kdl_arg *
|
||||
arg_new(const struct st_token *t)
|
||||
{
|
||||
struct st_kdl_arg *a = calloc(1, sizeof(*a));
|
||||
|
||||
if (a == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
ref_from_token(&a->value, t);
|
||||
return a;
|
||||
}
|
||||
|
||||
static struct st_kdl_prop *
|
||||
prop_new(const struct st_token *key, const struct st_token *value)
|
||||
{
|
||||
struct st_kdl_prop *pr = calloc(1, sizeof(*pr));
|
||||
|
||||
if (pr == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
ref_from_token(&pr->key, key);
|
||||
ref_from_token(&pr->value, value);
|
||||
return pr;
|
||||
}
|
||||
|
||||
static struct st_kdl_node *
|
||||
node_new(const struct st_token *name, const struct st_token *type,
|
||||
bool has_type)
|
||||
{
|
||||
struct st_kdl_node *n = calloc(1, sizeof(*n));
|
||||
|
||||
if (n == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
ref_from_token(&n->name, name);
|
||||
if (has_type) {
|
||||
n->type = malloc(sizeof(*n->type));
|
||||
if (n->type == NULL) {
|
||||
free(n);
|
||||
return NULL;
|
||||
}
|
||||
ref_from_token(n->type, type);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void
|
||||
nodes_free(struct st_kdl_node *head);
|
||||
|
||||
static void
|
||||
node_free(struct st_kdl_node *n)
|
||||
{
|
||||
struct st_kdl_arg *a;
|
||||
struct st_kdl_prop *pr;
|
||||
|
||||
if (n == NULL) {
|
||||
return;
|
||||
}
|
||||
free(n->type);
|
||||
for (a = n->args; a != NULL;) {
|
||||
struct st_kdl_arg *nx = a->next;
|
||||
free(a);
|
||||
a = nx;
|
||||
}
|
||||
for (pr = n->props; pr != NULL;) {
|
||||
struct st_kdl_prop *px = pr->next;
|
||||
free(pr);
|
||||
pr = px;
|
||||
}
|
||||
nodes_free(n->children);
|
||||
free(n);
|
||||
}
|
||||
|
||||
static void
|
||||
nodes_free(struct st_kdl_node *head)
|
||||
{
|
||||
while (head != NULL) {
|
||||
struct st_kdl_node *nx = head->next;
|
||||
node_free(head);
|
||||
head = nx;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- the grammar ------------------------------------------------------ */
|
||||
|
||||
static struct st_kdl_node *parse_node(struct st_parser *p,
|
||||
struct st_error **err);
|
||||
|
||||
/* Parse a children block. The opening `{` is already consumed; `lbrace`
|
||||
* is its span (used for the unterminated-block error). Returns the child
|
||||
* list head (NULL for an empty block), or NULL with *err set. */
|
||||
static struct st_kdl_node *
|
||||
parse_children(struct st_parser *p, struct st_span lbrace,
|
||||
struct st_error **err)
|
||||
{
|
||||
struct st_kdl_node *head = NULL;
|
||||
struct st_kdl_node *tail = NULL;
|
||||
|
||||
for (;;) {
|
||||
enum st_token_kind k;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
if (skip_trivia(p, err) < 0) {
|
||||
nodes_free(head);
|
||||
return NULL;
|
||||
}
|
||||
k = p_peek(p, err);
|
||||
if (k == ST_TOK_ERROR) {
|
||||
nodes_free(head);
|
||||
return NULL;
|
||||
}
|
||||
if (k == ST_TOK_EOF) {
|
||||
nodes_free(head);
|
||||
*err = err_at_owned(lbrace,
|
||||
"unterminated children block (missing closing '}')");
|
||||
return NULL;
|
||||
}
|
||||
if (k == ST_TOK_RBRACE) {
|
||||
p_consume(p);
|
||||
return head;
|
||||
}
|
||||
n = parse_node(p, err);
|
||||
if (n == NULL) {
|
||||
nodes_free(head);
|
||||
return NULL;
|
||||
}
|
||||
if (tail == NULL) {
|
||||
head = tail = n;
|
||||
} else {
|
||||
tail->next = n;
|
||||
tail = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse one node. Returns NULL with *err set on failure. The caller is
|
||||
* responsible for freeing the node list when NULL comes back. */
|
||||
static struct st_kdl_node *
|
||||
parse_node(struct st_parser *p, struct st_error **err)
|
||||
{
|
||||
struct st_kdl_node *n = NULL;
|
||||
struct st_token name_tok;
|
||||
struct st_token type_tok;
|
||||
struct st_kdl_arg *args_tail = NULL;
|
||||
struct st_kdl_prop *props_tail = NULL;
|
||||
bool has_type = false;
|
||||
size_t last_line;
|
||||
enum st_token_kind k;
|
||||
|
||||
/* Optional type annotation. */
|
||||
if (skip_comments(p, err) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
k = p_peek(p, err);
|
||||
if (k == ST_TOK_ERROR) {
|
||||
return NULL;
|
||||
}
|
||||
if (k == ST_TOK_TYPE) {
|
||||
type_tok = p->tok;
|
||||
has_type = true;
|
||||
p_consume(p);
|
||||
if (skip_comments(p, err) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
k = p_peek(p, err);
|
||||
if (k == ST_TOK_ERROR) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Name: any string-ish token. */
|
||||
if (!tok_is_stringish(k)) {
|
||||
*err = err_at_owned(p->tok.span, "expected a node name");
|
||||
return NULL;
|
||||
}
|
||||
name_tok = p->tok;
|
||||
p_consume(p);
|
||||
|
||||
n = node_new(&name_tok, &type_tok, has_type);
|
||||
if (n == NULL) {
|
||||
*err = err_at_owned(name_tok.span, "out of memory");
|
||||
return NULL;
|
||||
}
|
||||
last_line = name_tok.span.line;
|
||||
|
||||
/* Entries: interleaved arguments and properties. */
|
||||
for (;;) {
|
||||
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;
|
||||
}
|
||||
switch (k) {
|
||||
case ST_TOK_EOF:
|
||||
case ST_TOK_RBRACE:
|
||||
case ST_TOK_SEMICOLON:
|
||||
case ST_TOK_SLASHDASH:
|
||||
/* Terminated: the peeked token belongs to the caller's level. */
|
||||
return n;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (p->tok.span.line > last_line) {
|
||||
/* Newline termination (the lexer discards newlines). */
|
||||
return n;
|
||||
}
|
||||
switch (k) {
|
||||
case ST_TOK_LBRACE: {
|
||||
struct st_span lbrace = p->tok.span;
|
||||
|
||||
p_consume(p);
|
||||
n->children = parse_children(p, lbrace, err);
|
||||
if (*err != NULL) {
|
||||
node_free(n);
|
||||
return NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
case ST_TOK_EQUALS:
|
||||
/* The previous token was the node name or a non-key value. */
|
||||
*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;
|
||||
default:
|
||||
if (!tok_is_value(k)) {
|
||||
*err = err_at_owned(p->tok.span,
|
||||
"expected an argument or property");
|
||||
node_free(n);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* A value-ish entry: classify as property (key `=` value) or
|
||||
* argument. */
|
||||
{
|
||||
struct st_token val_tok = p->tok;
|
||||
|
||||
p_consume(p);
|
||||
if (tok_is_stringish(val_tok.kind)) {
|
||||
/* `=` lookahead, same source line only. */
|
||||
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 (k == ST_TOK_EQUALS &&
|
||||
p->tok.span.line == val_tok.span.line) {
|
||||
struct st_kdl_prop *pr;
|
||||
|
||||
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 '='");
|
||||
node_free(n);
|
||||
return NULL;
|
||||
}
|
||||
pr = prop_new(&val_tok, &p->tok);
|
||||
if (pr == NULL) {
|
||||
*err = err_at_owned(val_tok.span, "out of memory");
|
||||
node_free(n);
|
||||
return NULL;
|
||||
}
|
||||
last_line = p->tok.span.line;
|
||||
p_consume(p);
|
||||
if (props_tail == NULL) {
|
||||
n->props = props_tail = pr;
|
||||
} else {
|
||||
props_tail->next = pr;
|
||||
props_tail = pr;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/* Argument. */
|
||||
{
|
||||
struct st_kdl_arg *a = arg_new(&val_tok);
|
||||
|
||||
if (a == NULL) {
|
||||
*err = err_at_owned(val_tok.span, "out of memory");
|
||||
node_free(n);
|
||||
return NULL;
|
||||
}
|
||||
last_line = val_tok.span.line;
|
||||
if (args_tail == NULL) {
|
||||
n->args = args_tail = a;
|
||||
} else {
|
||||
args_tail->next = a;
|
||||
args_tail = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- public API ------------------------------------------------------- */
|
||||
|
||||
struct st_kdl_document *
|
||||
st_kdl_parse(const char *src, const char *filename, struct st_error **err)
|
||||
{
|
||||
struct st_parser parser;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *head = NULL;
|
||||
struct st_kdl_node *tail = NULL;
|
||||
|
||||
if (err != NULL) {
|
||||
*err = NULL;
|
||||
}
|
||||
|
||||
parser.lx = st_lexer_new(src, filename);
|
||||
if (parser.lx == NULL) {
|
||||
if (err != NULL) {
|
||||
*err = err_at_owned((struct st_span){ filename, 0, 0 },
|
||||
"out of memory");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
parser.has_tok = false;
|
||||
|
||||
doc = calloc(1, sizeof(*doc));
|
||||
if (doc == NULL) {
|
||||
st_lexer_free(parser.lx);
|
||||
if (err != NULL) {
|
||||
*err = err_at_owned((struct st_span){ filename, 0, 0 },
|
||||
"out of memory");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
enum st_token_kind k;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
if (skip_trivia(&parser, err) < 0) {
|
||||
goto fail;
|
||||
}
|
||||
k = p_peek(&parser, err);
|
||||
if (k == ST_TOK_ERROR) {
|
||||
goto fail;
|
||||
}
|
||||
if (k == ST_TOK_EOF) {
|
||||
break;
|
||||
}
|
||||
if (k == ST_TOK_RBRACE) {
|
||||
*err = err_at_owned(parser.tok.span, "unexpected '}'");
|
||||
goto fail;
|
||||
}
|
||||
n = parse_node(&parser, err);
|
||||
if (n == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
if (tail == NULL) {
|
||||
head = tail = n;
|
||||
} else {
|
||||
tail->next = n;
|
||||
tail = n;
|
||||
}
|
||||
}
|
||||
|
||||
st_lexer_free(parser.lx);
|
||||
doc->nodes = head;
|
||||
return doc;
|
||||
|
||||
fail:
|
||||
st_lexer_free(parser.lx);
|
||||
nodes_free(head);
|
||||
free(doc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
st_kdl_document_free(struct st_kdl_document *doc)
|
||||
{
|
||||
if (doc == NULL) {
|
||||
return;
|
||||
}
|
||||
nodes_free(doc->nodes);
|
||||
free(doc);
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
/* Parser fixture: a realistic multi-node build file exercising node names,
|
||||
arguments, properties, children blocks, comments, a slashdash, and
|
||||
semicolon separators. Parsed by tests/unit/test_parser.c against an
|
||||
exact expected AST (counts, order, nesting, spans). */
|
||||
|
||||
// top-level line comment
|
||||
project name="stupidtools" version="1.0.0"
|
||||
|
||||
target "default" {
|
||||
src "src/main.c" ; src "src/kdl/lexer.c"
|
||||
cc-flags "-std=c23" "-Wall" // pinned warning flags
|
||||
jobs 8
|
||||
optimize #true debug=#false
|
||||
feature "unit-tests" {
|
||||
option "munit" { default #true }
|
||||
}
|
||||
}
|
||||
|
||||
/-
|
||||
|
||||
(meta)credits "huntedbytheirs"
|
||||
@@ -0,0 +1,690 @@
|
||||
/* LINK: ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
|
||||
/* tests/unit/test_parser.c
|
||||
*
|
||||
* Unit tests for the KDL 2.0.0 recursive-descent parser (todo 7).
|
||||
*
|
||||
* 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). parser.c depends on lexer.c (token
|
||||
* stream), error.c (typed errors) and span.c (caret rendering).
|
||||
*
|
||||
* These tests assert on the REAL AST structure — node counts, argument
|
||||
* and property order, children nesting, spans — never just "no crash".
|
||||
* AST token slices are borrowed from the source buffer, so the source
|
||||
* must outlive the document (both kept alive until st_kdl_document_free).
|
||||
*/
|
||||
#include "munit.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/lexer.h"
|
||||
#include "span.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- fixture loading -------------------------------------------------- */
|
||||
|
||||
/* Locate tests/fixtures/basic.kdl. The harness runs the test binary with
|
||||
* cwd = repo top (make check) or tests/unit (manual run); probe both. */
|
||||
static const char *
|
||||
fixture_path(void)
|
||||
{
|
||||
static const char *const candidates[] = {
|
||||
"tests/fixtures/basic.kdl",
|
||||
"../fixtures/basic.kdl",
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
FILE *f = fopen(candidates[i], "rb");
|
||||
if (f != NULL) {
|
||||
fclose(f);
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Slurp the fixture into a NUL-terminated buffer (the lexer requires NUL
|
||||
* termination). Returns NULL on any I/O failure. Caller frees. */
|
||||
static char *
|
||||
slurp_fixture(const char *path)
|
||||
{
|
||||
FILE *f;
|
||||
long n;
|
||||
char *buf;
|
||||
|
||||
if (path == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
f = fopen(path, "rb");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
n = ftell(f);
|
||||
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
buf = munit_malloc((size_t)n + 1);
|
||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
|
||||
free(buf);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
buf[n] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ---- token-ref assertions --------------------------------------------- */
|
||||
|
||||
static void
|
||||
assert_ref(const struct st_kdl_token_ref *ref, enum st_token_kind kind,
|
||||
const char *text, size_t line, size_t col)
|
||||
{
|
||||
munit_assert_not_null(ref);
|
||||
munit_assert_int(ref->kind, ==, kind);
|
||||
if (text != NULL) {
|
||||
munit_assert_not_null(ref->text);
|
||||
munit_assert_size(ref->len, ==, strlen(text));
|
||||
munit_assert_memory_equal(ref->len, ref->text, text);
|
||||
}
|
||||
munit_assert_size(ref->span.line, ==, line);
|
||||
munit_assert_size(ref->span.col, ==, col);
|
||||
}
|
||||
|
||||
/* ---- tests ------------------------------------------------------------ */
|
||||
|
||||
/* (a) tests/fixtures/basic.kdl parses to the expected AST: node count,
|
||||
* names, arg order, prop key/value kinds, children nesting, spans. */
|
||||
static MunitResult
|
||||
test_parse_fixture(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
const char *path = fixture_path();
|
||||
char *src;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
struct st_kdl_prop *p;
|
||||
|
||||
munit_assert_not_null(path);
|
||||
src = slurp_fixture(path);
|
||||
munit_assert_not_null(src);
|
||||
|
||||
doc = st_kdl_parse(src, path, &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
/* --- three top-level nodes: project, target, credits ------------- */
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "project", 7, 1);
|
||||
|
||||
munit_assert_null(n->args);
|
||||
p = n->props;
|
||||
munit_assert_not_null(p);
|
||||
assert_ref(&p->key, ST_TOK_IDENT, "name", 7, 9);
|
||||
assert_ref(&p->value, ST_TOK_STRING, "\"stupidtools\"", 7, 14);
|
||||
p = p->next;
|
||||
munit_assert_not_null(p);
|
||||
assert_ref(&p->key, ST_TOK_IDENT, "version", 7, 28);
|
||||
assert_ref(&p->value, ST_TOK_STRING, "\"1.0.0\"", 7, 36);
|
||||
munit_assert_null(p->next);
|
||||
|
||||
/* --- target "default" with 6 children ---------------------------- */
|
||||
n = n->next;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "target", 9, 1);
|
||||
munit_assert_not_null(n->args);
|
||||
assert_ref(&n->args->value, ST_TOK_STRING, "\"default\"", 9, 8);
|
||||
munit_assert_null(n->args->next);
|
||||
munit_assert_null(n->props);
|
||||
|
||||
{
|
||||
struct st_kdl_node *c = n->children;
|
||||
|
||||
/* child 1: src "src/main.c" */
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "src", 10, 5);
|
||||
munit_assert_not_null(c->args);
|
||||
assert_ref(&c->args->value, ST_TOK_STRING, "\"src/main.c\"", 10, 9);
|
||||
munit_assert_null(c->args->next);
|
||||
|
||||
/* child 2: src "src/kdl/lexer.c" (after ';') */
|
||||
c = c->next;
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "src", 10, 24);
|
||||
assert_ref(&c->args->value, ST_TOK_STRING, "\"src/kdl/lexer.c\"", 10,
|
||||
28);
|
||||
munit_assert_null(c->args->next);
|
||||
|
||||
/* child 3: cc-flags "-std=c23" "-Wall" */
|
||||
c = c->next;
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "cc-flags", 11, 5);
|
||||
munit_assert_not_null(c->args);
|
||||
assert_ref(&c->args->value, ST_TOK_STRING, "\"-std=c23\"", 11, 14);
|
||||
assert_ref(&c->args->next->value, ST_TOK_STRING, "\"-Wall\"", 11, 25);
|
||||
munit_assert_null(c->args->next->next);
|
||||
|
||||
/* child 4: jobs 8 */
|
||||
c = c->next;
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "jobs", 12, 5);
|
||||
assert_ref(&c->args->value, ST_TOK_NUMBER, "8", 12, 10);
|
||||
munit_assert_null(c->args->next);
|
||||
|
||||
/* child 5: optimize #true debug=#false */
|
||||
c = c->next;
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "optimize", 13, 5);
|
||||
assert_ref(&c->args->value, ST_TOK_TRUE, "#true", 13, 14);
|
||||
munit_assert_null(c->args->next);
|
||||
munit_assert_not_null(c->props);
|
||||
assert_ref(&c->props->key, ST_TOK_IDENT, "debug", 13, 20);
|
||||
assert_ref(&c->props->value, ST_TOK_FALSE, "#false", 13, 26);
|
||||
munit_assert_null(c->props->next);
|
||||
|
||||
/* child 6: feature "unit-tests" { option "munit" { default #true } } */
|
||||
c = c->next;
|
||||
munit_assert_not_null(c);
|
||||
assert_ref(&c->name, ST_TOK_IDENT, "feature", 14, 5);
|
||||
assert_ref(&c->args->value, ST_TOK_STRING, "\"unit-tests\"", 14, 13);
|
||||
munit_assert_null(c->args->next);
|
||||
munit_assert_not_null(c->children);
|
||||
assert_ref(&c->children->name, ST_TOK_IDENT, "option", 15, 9);
|
||||
assert_ref(&c->children->args->value, ST_TOK_STRING, "\"munit\"", 15,
|
||||
16);
|
||||
munit_assert_not_null(c->children->children);
|
||||
assert_ref(&c->children->children->name, ST_TOK_IDENT, "default", 15,
|
||||
26);
|
||||
assert_ref(&c->children->children->args->value, ST_TOK_TRUE, "#true",
|
||||
15, 34);
|
||||
munit_assert_null(c->children->children->args->next);
|
||||
munit_assert_null(c->next);
|
||||
}
|
||||
|
||||
/* --- credits with type annotation (meta) -------------------------- */
|
||||
n = n->next;
|
||||
munit_assert_not_null(n);
|
||||
munit_assert_not_null(n->type);
|
||||
/* the TYPE token's span starts at the opening '('; its text slice is
|
||||
* the inner identifier without the parens (see lexer.h) */
|
||||
assert_ref(n->type, ST_TOK_TYPE, "meta", 21, 1);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "credits", 21, 7);
|
||||
assert_ref(&n->args->value, ST_TOK_STRING, "\"huntedbytheirs\"", 21, 15);
|
||||
munit_assert_null(n->args->next);
|
||||
munit_assert_null(n->props);
|
||||
munit_assert_null(n->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
free(src);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (b) `node {` — unterminated children block errors with the span of the
|
||||
* `{` token. */
|
||||
static MunitResult
|
||||
test_unterminated_children(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
const char *src = "node {";
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse(src, "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_PARSE);
|
||||
/* the span must point at the `{` (line 1, col 6), not at EOF */
|
||||
munit_assert_not_null(err->span);
|
||||
munit_assert_size(err->span->line, ==, 1);
|
||||
munit_assert_size(err->span->col, ==, 6);
|
||||
munit_assert_true(strstr(st_error_message(err), "children") != NULL);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (c) a stray `}` at top level errors with the span of the `}`. */
|
||||
static MunitResult
|
||||
test_stray_rbrace(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("}", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
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_size(err->span->line, ==, 1);
|
||||
munit_assert_size(err->span->col, ==, 1);
|
||||
st_error_free(err);
|
||||
|
||||
/* also stray inside a completed children block: `node { child } }` */
|
||||
err = NULL;
|
||||
doc = st_kdl_parse("node { child } }", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_size(err->span->col, ==, 16);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (d) prop classification: a token followed by `=` is a key, the next
|
||||
* value token is its value; bare values are args, in source order. */
|
||||
static MunitResult
|
||||
test_prop_classification(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
struct st_kdl_arg *a;
|
||||
struct st_kdl_prop *p;
|
||||
|
||||
doc = st_kdl_parse("node key=value key2=\"v2\" 1 2 #true", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
munit_assert_null(n->next);
|
||||
|
||||
p = n->props;
|
||||
munit_assert_not_null(p);
|
||||
assert_ref(&p->key, ST_TOK_IDENT, "key", 1, 6);
|
||||
assert_ref(&p->value, ST_TOK_IDENT, "value", 1, 10);
|
||||
p = p->next;
|
||||
munit_assert_not_null(p);
|
||||
assert_ref(&p->key, ST_TOK_IDENT, "key2", 1, 16);
|
||||
assert_ref(&p->value, ST_TOK_STRING, "\"v2\"", 1, 21);
|
||||
munit_assert_null(p->next);
|
||||
|
||||
a = n->args;
|
||||
munit_assert_not_null(a);
|
||||
assert_ref(&a->value, ST_TOK_NUMBER, "1", 1, 26);
|
||||
a = a->next;
|
||||
munit_assert_not_null(a);
|
||||
assert_ref(&a->value, ST_TOK_NUMBER, "2", 1, 28);
|
||||
a = a->next;
|
||||
munit_assert_not_null(a);
|
||||
assert_ref(&a->value, ST_TOK_TRUE, "#true", 1, 30);
|
||||
munit_assert_null(a->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (e) interleaved args/props keep their relative order within each list:
|
||||
* `node a=1 b c=2 d` -> args [b, d], props [a=1, c=2]. */
|
||||
static MunitResult
|
||||
test_interleaved_order(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
doc = st_kdl_parse("node a=1 b c=2 d", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
|
||||
/* args, in order: b then d */
|
||||
munit_assert_not_null(n->args);
|
||||
assert_ref(&n->args->value, ST_TOK_IDENT, "b", 1, 10);
|
||||
assert_ref(&n->args->next->value, ST_TOK_IDENT, "d", 1, 16);
|
||||
munit_assert_null(n->args->next->next);
|
||||
|
||||
/* props, in order: a=1 then c=2 */
|
||||
munit_assert_not_null(n->props);
|
||||
assert_ref(&n->props->key, ST_TOK_IDENT, "a", 1, 6);
|
||||
assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 1, 8);
|
||||
assert_ref(&n->props->next->key, ST_TOK_IDENT, "c", 1, 12);
|
||||
assert_ref(&n->props->next->value, ST_TOK_NUMBER, "2", 1, 14);
|
||||
munit_assert_null(n->props->next->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (f) newline terminates a node: `a 1\nb 2` is two nodes, not one node
|
||||
* with four entries. */
|
||||
static MunitResult
|
||||
test_newline_separates_nodes(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
doc = st_kdl_parse("a 1\nb 2", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "a", 1, 1);
|
||||
assert_ref(&n->args->value, ST_TOK_NUMBER, "1", 1, 3);
|
||||
munit_assert_null(n->args->next);
|
||||
|
||||
n = n->next;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "b", 2, 1);
|
||||
assert_ref(&n->args->value, ST_TOK_NUMBER, "2", 2, 3);
|
||||
munit_assert_null(n->args->next);
|
||||
munit_assert_null(n->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (g) semicolons separate nodes; repeated separators are trivia. */
|
||||
static MunitResult
|
||||
test_semicolon_separators(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
size_t count = 0;
|
||||
|
||||
doc = st_kdl_parse("node ; ; node", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
for (n = doc->nodes; n != NULL; n = n->next) {
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "node", 1,
|
||||
(size_t)(count == 0 ? 1 : 10));
|
||||
count++;
|
||||
}
|
||||
munit_assert_size(count, ==, 2);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (h) an empty document parses to a document with zero nodes. */
|
||||
static MunitResult
|
||||
test_empty_document(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
munit_assert_null(doc->nodes);
|
||||
st_kdl_document_free(doc);
|
||||
|
||||
/* whitespace/comment-only is also an empty document */
|
||||
err = NULL;
|
||||
doc = st_kdl_parse(" // nothing\n/* nor here */\n/- ;", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
munit_assert_null(doc->nodes);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (i) `node = x` is invalid: the node name cannot be a property key. */
|
||||
static MunitResult
|
||||
test_equals_after_name(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("node = x", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
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_size(err->span->line, ==, 1);
|
||||
munit_assert_size(err->span->col, ==, 6);
|
||||
st_error_free(err);
|
||||
|
||||
/* a non-string value before `=` is equally invalid: `node 1 = 2` */
|
||||
err = NULL;
|
||||
doc = st_kdl_parse("node 1 = 2", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_size(err->span->col, ==, 8);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (j) a property needs a value: `node key =` errors at EOF; `node key = {`
|
||||
* errors at the `{`. */
|
||||
static MunitResult
|
||||
test_prop_missing_value(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("node key =", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_true(strstr(st_error_message(err), "value") != NULL);
|
||||
munit_assert_not_null(err->span);
|
||||
munit_assert_size(err->span->col, ==, 11);
|
||||
st_error_free(err);
|
||||
|
||||
err = NULL;
|
||||
doc = st_kdl_parse("node key = {", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_true(strstr(st_error_message(err), "value") != NULL);
|
||||
munit_assert_size(err->span->col, ==, 12);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (k) a lexer error is propagated with its span, copied into owned
|
||||
* storage: the span must still be readable after st_kdl_parse returns. */
|
||||
static MunitResult
|
||||
test_lexer_error_propagates(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("node \"unterminated", "t.kdl", &err);
|
||||
munit_assert_null(doc);
|
||||
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_size(err->span->line, ==, 1);
|
||||
munit_assert_size(err->span->col, ==, 6);
|
||||
munit_assert_true(strstr(st_error_message(err), "unterminated") != NULL);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (l) type annotation attaches to the node name; quoted node names and
|
||||
* string keys work. */
|
||||
static MunitResult
|
||||
test_annotation_and_string_names(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
doc = st_kdl_parse("(u8)size 4\n\"quoted node\" x\np \"k s\"=1", "t.kdl",
|
||||
&err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
munit_assert_not_null(n->type);
|
||||
assert_ref(n->type, ST_TOK_TYPE, "u8", 1, 1);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "size", 1, 5);
|
||||
assert_ref(&n->args->value, ST_TOK_NUMBER, "4", 1, 10);
|
||||
munit_assert_null(n->args->next);
|
||||
|
||||
n = n->next;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_STRING, "\"quoted node\"", 2, 1);
|
||||
assert_ref(&n->args->value, ST_TOK_IDENT, "x", 2, 15);
|
||||
munit_assert_null(n->args->next);
|
||||
|
||||
n = n->next;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "p", 3, 1);
|
||||
munit_assert_not_null(n->props);
|
||||
assert_ref(&n->props->key, ST_TOK_STRING, "\"k s\"", 3, 3);
|
||||
assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 3, 9);
|
||||
munit_assert_null(n->props->next);
|
||||
munit_assert_null(n->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (m) single-line children blocks and nested blocks parse recursively. */
|
||||
static MunitResult
|
||||
test_children_nesting(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
|
||||
doc = st_kdl_parse("p { a; b; c { d } }", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "p", 1, 1);
|
||||
munit_assert_not_null(n->children);
|
||||
assert_ref(&n->children->name, ST_TOK_IDENT, "a", 1, 5);
|
||||
assert_ref(&n->children->next->name, ST_TOK_IDENT, "b", 1, 8);
|
||||
assert_ref(&n->children->next->next->name, ST_TOK_IDENT, "c", 1, 11);
|
||||
munit_assert_not_null(n->children->next->next->children);
|
||||
assert_ref(&n->children->next->next->children->name, ST_TOK_IDENT, "d",
|
||||
1, 15);
|
||||
munit_assert_null(n->children->next->next->next);
|
||||
munit_assert_null(n->next);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (n) `/-` and comments are trivia: a slashdash between entries vanishes,
|
||||
* and a node ends at the slashdash/semicolon regardless of line. */
|
||||
static MunitResult
|
||||
test_trivia_skipped(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
size_t count = 0;
|
||||
|
||||
doc = st_kdl_parse("node a=1 /-\n; // still trivia\nb", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
n = doc->nodes;
|
||||
munit_assert_not_null(n);
|
||||
assert_ref(&n->name, ST_TOK_IDENT, "node", 1, 1);
|
||||
munit_assert_not_null(n->props);
|
||||
assert_ref(&n->props->key, ST_TOK_IDENT, "a", 1, 6);
|
||||
assert_ref(&n->props->value, ST_TOK_NUMBER, "1", 1, 8);
|
||||
munit_assert_null(n->props->next);
|
||||
munit_assert_null(n->args);
|
||||
|
||||
for (n = n->next; n != NULL; n = n->next) {
|
||||
count++;
|
||||
}
|
||||
munit_assert_size(count, ==, 1);
|
||||
assert_ref(&doc->nodes->next->name, ST_TOK_IDENT, "b", 3, 1);
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (o) `st_kdl_document_free(NULL)` is a safe no-op. */
|
||||
static MunitResult
|
||||
test_free_null(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
st_kdl_document_free(NULL);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitTest tests[] = {
|
||||
{ "/parse/fixture-basic-kdl", test_parse_fixture, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/unterminated-children", test_unterminated_children, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/stray-rbrace", test_stray_rbrace, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/prop-classification", test_prop_classification, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/interleaved-order", test_interleaved_order, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/newline-separates-nodes", test_newline_separates_nodes, NULL,
|
||||
NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/semicolon-separators", test_semicolon_separators, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/empty-document", test_empty_document, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/equals-after-name", test_equals_after_name, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/prop-missing-value", test_prop_missing_value, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/lexer-error-propagates", test_lexer_error_propagates, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/annotation-and-string-names", test_annotation_and_string_names,
|
||||
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/children-nesting", test_children_nesting, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/trivia-skipped", test_trivia_skipped, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/parse/free-null", test_free_null, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
};
|
||||
|
||||
static const MunitSuite suite = {
|
||||
"/parser", 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);
|
||||
}
|
||||
Reference in New Issue
Block a user