feat(kdl): add KDL lexer with source spans

This commit is contained in:
2026-08-28 20:28:57 -04:00
parent 468aa1c4ef
commit 50afaa17ca
3 changed files with 1686 additions and 0 deletions
+943
View File
@@ -0,0 +1,943 @@
/*
* lexer.c - KDL 2.0.0 tokenizer for stupidtools (todo 6).
*
* Produces a FLAT token stream with a source span on every token. It does
* NOT parse node/argument/property structure (the parser does, todo 7).
* See lexer.h for the full API + error-span-lifetime contract.
*
* Design decisions (mirrored in .omo/notepads/stupidtools/learnings.md):
* - identifiers are scanned greedily; non-ASCII bytes are identifier
* characters (the non-identifier set is the ASCII one from §3.10.2).
* - bare `inf` / `-inf` / `nan` / `true` / `false` / `null` are syntax
* errors; the `#`-prefixed keyword forms are distinct token kinds.
* - multi-line strings are delimited by `"""` only; the spec's "first
* line must be a newline" + dedent-prefix rules are VALUE rules and
* are deferred to the value model (todo 8), so `"""multi"""` tokenizes.
* - columns are byte-based (not codepoints); full Unicode grapheme
* columns and disallowed-literal-code-point validation are deferred.
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "kdl/lexer.h"
#include "error.h"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
struct st_lexer {
const char *src; /* borrowed source buffer (NUL-terminated) */
const char *pos; /* current scan position */
const char *filename; /* borrowed; stamped into every span */
size_t line; /* current 1-based line */
size_t col; /* current 1-based column (bytes) */
struct st_span err_span; /* stable span for the most recent error */
};
/*
* Character classes. Non-ASCII bytes are deliberately treated as
* identifier characters (see header note); the classification below only
* special-cases ASCII + the UTF-8 newline sequences (NEL/LS/PS).
*/
static bool
is_digit(unsigned char c)
{
return c >= '0' && c <= '9';
}
static bool
is_hex_digit(unsigned char c)
{
return is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
static bool
is_oct_digit(unsigned char c)
{
return c >= '0' && c <= '7';
}
static bool
is_bin_digit(unsigned char c)
{
return c == '0' || c == '1';
}
/* Non-newline whitespace (ASCII subset; full Unicode White_Space deferred). */
static bool
is_space(unsigned char c)
{
return c == ' ' || c == '\t';
}
/* Non-identifier ASCII chars per KDL §3.10.2: \ / ( ) { } ; [ ] " # = */
static bool
is_nonident(unsigned char c)
{
switch (c) {
case '\\':
case '/':
case '(':
case ')':
case '{':
case '}':
case ';':
case '[':
case ']':
case '"':
case '#':
case '=':
return true;
default:
return false;
}
}
/* Newline sequences per KDL §3.18: CRLF (one newline), CR, LF, VT, FF,
* NEL (U+0085), LS (U+2028), PS (U+2029). */
static bool
is_newline_at(const char *p)
{
unsigned char c = (unsigned char)p[0];
if (c == '\r' || c == '\n' || c == '\v' || c == '\f') {
return true;
}
if (c == 0xC2 && (unsigned char)p[1] == 0x85) {
return true; /* NEL */
}
if (c == 0xE2 && (unsigned char)p[1] == 0x80 &&
((unsigned char)p[2] == 0xA8 || (unsigned char)p[2] == 0xA9)) {
return true; /* LS, PS */
}
return false;
}
/* Advance one character, updating line/col. CRLF counts as one newline. */
static void
advance(struct st_lexer *lx)
{
unsigned char c = (unsigned char)*lx->pos;
if (c == '\0') {
return;
}
if (is_newline_at(lx->pos)) {
if (c == '\r' && lx->pos[1] == '\n') {
lx->pos += 2;
} else if (c == 0xC2) {
lx->pos += 2;
} else if (c == 0xE2) {
lx->pos += 3;
} else {
lx->pos += 1;
}
lx->line++;
lx->col = 1;
return;
}
lx->pos += 1;
lx->col += 1;
}
/* True when the cursor is on a byte that may continue an identifier. */
static bool
at_ident_char(const struct st_lexer *lx)
{
unsigned char c = (unsigned char)*lx->pos;
if (c == '\0' || is_nonident(c) || is_space(c)) {
return false;
}
return !is_newline_at(lx->pos);
}
/* True when the cursor is on a byte that may START an identifier (i.e. not
* a digit, non-ident char, whitespace, or newline). `+`/`-`/`.` are handled
* by the caller because they can also begin numbers. */
static bool
at_ident_start(const struct st_lexer *lx)
{
unsigned char c = (unsigned char)*lx->pos;
if (c == '\0' || is_digit(c) || is_nonident(c) || is_space(c)) {
return false;
}
return !is_newline_at(lx->pos);
}
/* Build a KDL-parse error at `sp` and return ST_TOK_ERROR. The span is
* stored in the lexer so the error's borrowed pointer stays valid until
* the next st_lexer_next/free (see lexer.h). */
static enum st_token_kind
fail(struct st_lexer *lx, struct st_span sp, const char *msg,
struct st_error **err)
{
if (err != NULL) {
lx->err_span = sp;
*err = st_error_at(st_error_kdl_parse(msg), &lx->err_span);
}
return ST_TOK_ERROR;
}
static void
emit(struct st_token *out, enum st_token_kind kind, struct st_span span,
const char *text, size_t len)
{
out->kind = kind;
out->span = span;
out->text = text;
out->len = len;
}
/* Is `s` (len bytes) one of the bare keyword identifiers that KDL forbids
* as an identifier string (must be written with `#` or quoted)? */
static bool
is_bare_keyword(const char *s, size_t len)
{
static const char *const keywords[] = {
"true", "false", "null", "inf", "-inf", "nan",
};
size_t i;
for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
size_t klen = strlen(keywords[i]);
if (len == klen && memcmp(s, keywords[i], klen) == 0) {
return true;
}
}
return false;
}
/* --- line continuations ------------------------------------------------ */
/* Consume an escline: '\' ws* (single-line-comment | newline | eof).
* Returns 0 on success, -1 on error (*err set). */
static int
lex_escline(struct st_lexer *lx, struct st_error **err)
{
struct st_span sp = { lx->filename, lx->line, lx->col };
advance(lx); /* '\' */
while (*lx->pos != '\0' && is_space((unsigned char)*lx->pos)) {
advance(lx);
}
if (*lx->pos == '\0') {
return 0; /* eof is a legal escline terminator */
}
if (is_newline_at(lx->pos)) {
advance(lx);
return 0;
}
if (lx->pos[0] == '/' && lx->pos[1] == '/') {
advance(lx);
advance(lx);
while (*lx->pos != '\0' && !is_newline_at(lx->pos)) {
advance(lx);
}
if (*lx->pos != '\0') {
advance(lx); /* the terminating newline */
}
return 0;
}
(void)fail(lx, sp,
"invalid line continuation: '\\' must be followed by a newline "
"or a line comment", err);
return -1;
}
/* --- identifiers ------------------------------------------------------- */
static enum st_token_kind
lex_ident(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *begin = lx->pos;
advance(lx); /* first char is a valid ident-start (caller guarantees) */
while (at_ident_char(lx)) {
advance(lx);
}
if (is_bare_keyword(begin, (size_t)(lx->pos - begin))) {
return fail(lx, start,
"bare keyword identifier; write it as a '#'-prefixed keyword "
"or a quoted string", err);
}
emit(out, ST_TOK_IDENT, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_IDENT;
}
/* --- numbers ----------------------------------------------------------- */
static enum st_token_kind
lex_number(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *begin = lx->pos;
if (*lx->pos == '+' || *lx->pos == '-') {
advance(lx); /* sign */
}
if (lx->pos[0] == '0' && (lx->pos[1] == 'x' || lx->pos[1] == 'X')) {
advance(lx);
advance(lx); /* 0x */
if (!is_hex_digit((unsigned char)*lx->pos)) {
return fail(lx, start,
"invalid hexadecimal number: expected a hex digit after "
"'0x'", err);
}
while (is_hex_digit((unsigned char)*lx->pos) ||
*lx->pos == '_') {
advance(lx);
}
} else if (lx->pos[0] == '0' &&
(lx->pos[1] == 'o' || lx->pos[1] == 'O')) {
advance(lx);
advance(lx); /* 0o */
if (!is_oct_digit((unsigned char)*lx->pos)) {
return fail(lx, start,
"invalid octal number: expected an octal digit after '0o'",
err);
}
while (is_oct_digit((unsigned char)*lx->pos) || *lx->pos == '_') {
advance(lx);
}
} else if (lx->pos[0] == '0' &&
(lx->pos[1] == 'b' || lx->pos[1] == 'B')) {
advance(lx);
advance(lx); /* 0b */
if (!is_bin_digit((unsigned char)*lx->pos)) {
return fail(lx, start,
"invalid binary number: expected a binary digit after '0b'",
err);
}
while (is_bin_digit((unsigned char)*lx->pos) || *lx->pos == '_') {
advance(lx);
}
} else {
/* decimal: integer ('.' integer)? exponent? */
while (is_digit((unsigned char)*lx->pos) || *lx->pos == '_') {
advance(lx);
}
if (*lx->pos == '.' && is_digit((unsigned char)lx->pos[1])) {
advance(lx); /* '.' */
while (is_digit((unsigned char)*lx->pos) || *lx->pos == '_') {
advance(lx);
}
}
if (*lx->pos == 'e' || *lx->pos == 'E') {
const char *save = lx->pos;
size_t save_line = lx->line;
size_t save_col = lx->col;
advance(lx); /* e */
if (*lx->pos == '+' || *lx->pos == '-') {
advance(lx);
}
if (!is_digit((unsigned char)*lx->pos)) {
/* not a real exponent: back off */
lx->pos = save;
lx->line = save_line;
lx->col = save_col;
} else {
while (is_digit((unsigned char)*lx->pos) ||
*lx->pos == '_') {
advance(lx);
}
}
}
}
emit(out, ST_TOK_NUMBER, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_NUMBER;
}
/* Dispatch for bytes that may start either a number or an identifier
* (digits, '+', '-', '.'). */
static enum st_token_kind
lex_value_start(struct st_lexer *lx, struct st_token *out,
struct st_span start, struct st_error **err)
{
unsigned char c = (unsigned char)*lx->pos;
if (is_digit(c)) {
return lex_number(lx, out, start, err);
}
if (c == '+' || c == '-') {
unsigned char n = (unsigned char)lx->pos[1];
if (is_digit(n)) {
return lex_number(lx, out, start, err);
}
if (n == '.' && is_digit((unsigned char)lx->pos[2])) {
return fail(lx, start,
"number must have a leading digit before the decimal point",
err);
}
return lex_ident(lx, out, start, err);
}
if (c == '.') {
if (is_digit((unsigned char)lx->pos[1])) {
return fail(lx, start,
"number must have a leading digit (write '0.1', not '.1')",
err);
}
return lex_ident(lx, out, start, err);
}
return lex_ident(lx, out, start, err);
}
/* --- strings ----------------------------------------------------------- */
/* Scan one escape sequence (at a '\'); validates it but does not unescape.
* Returns 0 on success, -1 on error. */
static int
lex_escape(struct st_lexer *lx, struct st_span start, struct st_error **err)
{
unsigned char c;
advance(lx); /* '\' */
c = (unsigned char)*lx->pos;
if (c == '\0') {
(void)fail(lx, start, "unterminated escape sequence in string", err);
return -1;
}
switch (c) {
case 'n':
case 'r':
case 't':
case '\\':
case '"':
case 'b':
case 'f':
case 's':
advance(lx);
return 0;
case 'u': {
int ndigits = 0;
advance(lx); /* u */
if (*lx->pos != '{') {
(void)fail(lx, start,
"invalid unicode escape: expected '{' after '\\u'", err);
return -1;
}
advance(lx); /* { */
while (is_hex_digit((unsigned char)*lx->pos)) {
advance(lx);
ndigits++;
}
if (*lx->pos != '}') {
(void)fail(lx, start,
"invalid unicode escape: expected '}'", err);
return -1;
}
if (ndigits == 0 || ndigits > 6) {
(void)fail(lx, start,
"invalid unicode escape: expected 1-6 hex digits", err);
return -1;
}
advance(lx); /* } */
return 0;
}
default:
/* whitespace escape: '\' + one-or-more whitespace/newlines */
if (is_space(c) || is_newline_at(lx->pos)) {
while (*lx->pos != '\0' &&
(is_space((unsigned char)*lx->pos) ||
is_newline_at(lx->pos))) {
advance(lx);
}
return 0;
}
(void)fail(lx, start, "invalid escape sequence in string", err);
return -1;
}
}
/* Single-line quoted string: "..." (raw disables escapes). */
static enum st_token_kind
lex_quoted_string(struct st_lexer *lx, struct st_token *out,
struct st_span start, struct st_error **err, bool raw)
{
const char *begin = lx->pos;
advance(lx); /* opening '"' */
for (;;) {
unsigned char c = (unsigned char)*lx->pos;
if (c == '\0') {
return fail(lx, start, "unterminated string literal", err);
}
if (c == '"') {
advance(lx); /* closing '"' */
break;
}
if (!raw && c == '\\') {
if (lex_escape(lx, start, err) < 0) {
return ST_TOK_ERROR;
}
continue;
}
if (is_newline_at(lx->pos)) {
return fail(lx, start,
"unterminated string literal (newline before closing quote)",
err);
}
advance(lx);
}
emit(out, ST_TOK_STRING, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_STRING;
}
/* Multi-line string: """...""" (raw disables escapes). Only the delimiters
* are recognized here; dedent/value rules are deferred to todo 8. */
static enum st_token_kind
lex_multiline_string(struct st_lexer *lx, struct st_token *out,
struct st_span start, struct st_error **err, bool raw)
{
const char *begin = lx->pos;
advance(lx);
advance(lx);
advance(lx); /* opening '"""' */
for (;;) {
if (*lx->pos == '\0') {
return fail(lx, start, "unterminated multi-line string", err);
}
if (lx->pos[0] == '"' && lx->pos[1] == '"' && lx->pos[2] == '"') {
advance(lx);
advance(lx);
advance(lx); /* closing '"""' */
break;
}
if (!raw && *lx->pos == '\\') {
if (lex_escape(lx, start, err) < 0) {
return ST_TOK_ERROR;
}
continue;
}
advance(lx);
}
emit(out, ST_TOK_MULTILINE_STRING, start, begin,
(size_t)(lx->pos - begin));
return ST_TOK_MULTILINE_STRING;
}
/* Raw string: '#'* '"' body '"' '#'* or '#'* '"""' body '"""' '#'*.
* The body may contain '"' / '#' as long as it is not the closing
* delimiter (the closing quote followed by exactly n '#'). */
static enum st_token_kind
lex_raw_string(struct st_lexer *lx, struct st_token *out,
struct st_span start, struct st_error **err)
{
const char *begin = lx->pos;
size_t n = 0;
size_t i;
bool multi;
while (*lx->pos == '#') {
advance(lx);
n++;
}
multi = (lx->pos[0] == '"' && lx->pos[1] == '"' && lx->pos[2] == '"');
if (multi) {
advance(lx);
advance(lx);
advance(lx); /* '"""' */
for (;;) {
if (*lx->pos == '\0') {
return fail(lx, start,
"unterminated raw multi-line string", err);
}
if (lx->pos[0] == '"' && lx->pos[1] == '"' &&
lx->pos[2] == '"') {
bool closes = true;
for (i = 0; i < n; i++) {
if (lx->pos[3 + i] != '#') {
closes = false;
break;
}
}
if (closes) {
advance(lx);
advance(lx);
advance(lx);
for (i = 0; i < n; i++) {
advance(lx);
}
break;
}
}
advance(lx);
}
} else {
advance(lx); /* '"' */
for (;;) {
if (*lx->pos == '\0') {
return fail(lx, start,
"unterminated raw string literal", err);
}
if (lx->pos[0] == '"') {
bool closes = true;
for (i = 0; i < n; i++) {
if (lx->pos[1 + i] != '#') {
closes = false;
break;
}
}
if (closes) {
advance(lx); /* '"' */
for (i = 0; i < n; i++) {
advance(lx);
}
break;
}
}
if (is_newline_at(lx->pos)) {
return fail(lx, start,
"unterminated raw string literal (newline before "
"closing quote)", err);
}
advance(lx);
}
}
emit(out, ST_TOK_RAW_STRING, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_RAW_STRING;
}
/* --- keywords (#inf / #-inf / #nan / #true / #false / #null) ----------- */
static enum st_token_kind
lex_keyword(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *begin = lx->pos;
const char *kw;
size_t kwlen;
advance(lx); /* '#' */
kw = lx->pos;
if (*lx->pos == '-') {
advance(lx);
}
while (at_ident_char(lx)) {
advance(lx);
}
kwlen = (size_t)(lx->pos - kw);
if (kwlen == 3 && memcmp(kw, "inf", 3) == 0) {
emit(out, ST_TOK_INF, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_INF;
}
if (kwlen == 4 && memcmp(kw, "-inf", 4) == 0) {
emit(out, ST_TOK_NEG_INF, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_NEG_INF;
}
if (kwlen == 3 && memcmp(kw, "nan", 3) == 0) {
emit(out, ST_TOK_NAN, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_NAN;
}
if (kwlen == 4 && memcmp(kw, "true", 4) == 0) {
emit(out, ST_TOK_TRUE, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_TRUE;
}
if (kwlen == 5 && memcmp(kw, "false", 5) == 0) {
emit(out, ST_TOK_FALSE, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_FALSE;
}
if (kwlen == 4 && memcmp(kw, "null", 4) == 0) {
emit(out, ST_TOK_NULL, start, begin, (size_t)(lx->pos - begin));
return ST_TOK_NULL;
}
return fail(lx, start, "unknown '#'-prefixed keyword", err);
}
/* Dispatch on '#': raw string or keyword. */
static enum st_token_kind
lex_hash(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *p = lx->pos;
while (*p == '#') {
p++;
}
if (*p == '"') {
return lex_raw_string(lx, out, start, err);
}
if (lx->pos[1] == '#') {
return fail(lx, start,
"invalid token: '#' must be followed by a string or a keyword",
err);
}
return lex_keyword(lx, out, start, err);
}
/* --- type annotation --------------------------------------------------- */
static enum st_token_kind
lex_type(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *name;
size_t namelen;
advance(lx); /* '(' */
while (is_space((unsigned char)*lx->pos)) {
advance(lx);
}
if (!at_ident_start(lx)) {
return fail(lx, start, "expected a type name inside '('", err);
}
name = lx->pos;
advance(lx);
while (at_ident_char(lx)) {
advance(lx);
}
namelen = (size_t)(lx->pos - name);
while (is_space((unsigned char)*lx->pos)) {
advance(lx);
}
if (*lx->pos != ')') {
return fail(lx, start,
"unterminated type annotation (expected ')')", err);
}
advance(lx); /* ')' */
emit(out, ST_TOK_TYPE, start, name, namelen);
return ST_TOK_TYPE;
}
/* --- comments ---------------------------------------------------------- */
static enum st_token_kind
lex_comment(struct st_lexer *lx, struct st_token *out, struct st_span start,
struct st_error **err)
{
const char *begin = lx->pos;
if (lx->pos[1] == '/') {
advance(lx);
advance(lx); /* '//' */
while (*lx->pos != '\0' && !is_newline_at(lx->pos)) {
advance(lx);
}
emit(out, ST_TOK_LINE_COMMENT, start, begin,
(size_t)(lx->pos - begin));
return ST_TOK_LINE_COMMENT;
}
if (lx->pos[1] == '*') {
int depth = 1;
advance(lx);
advance(lx); /* slash-star opener */
while (depth > 0) {
if (*lx->pos == '\0') {
return fail(lx, start, "unterminated block comment", err);
}
if (lx->pos[0] == '/' && lx->pos[1] == '*') {
advance(lx);
advance(lx);
depth++;
continue;
}
if (lx->pos[0] == '*' && lx->pos[1] == '/') {
advance(lx);
advance(lx);
depth--;
continue;
}
advance(lx);
}
emit(out, ST_TOK_BLOCK_COMMENT, start, begin,
(size_t)(lx->pos - begin));
return ST_TOK_BLOCK_COMMENT;
}
if (lx->pos[1] == '-') {
advance(lx);
advance(lx); /* '/-' */
emit(out, ST_TOK_SLASHDASH, start, begin,
(size_t)(lx->pos - begin));
return ST_TOK_SLASHDASH;
}
return fail(lx, start, "unexpected '/'", err);
}
/* --- public API -------------------------------------------------------- */
struct st_lexer *
st_lexer_new(const char *src, const char *filename)
{
struct st_lexer *lx = calloc(1, sizeof(*lx));
if (lx == NULL) {
return NULL;
}
lx->src = src != NULL ? src : "";
lx->pos = lx->src;
lx->filename = filename;
lx->line = 1;
lx->col = 1;
/* Skip a leading BOM (U+FEFF = EF BB BF), legal only as the very first
* code point of a document. */
if ((unsigned char)lx->pos[0] == 0xEF &&
(unsigned char)lx->pos[1] == 0xBB &&
(unsigned char)lx->pos[2] == 0xBF) {
lx->pos += 3;
}
return lx;
}
void
st_lexer_free(struct st_lexer *lx)
{
free(lx);
}
enum st_token_kind
st_lexer_next(struct st_lexer *lx, struct st_token *out, struct st_error **err)
{
unsigned char c;
struct st_span start;
if (err != NULL) {
*err = NULL;
}
/* Skip whitespace, newlines, and line continuations. */
for (;;) {
while (*lx->pos != '\0' &&
(is_space((unsigned char)*lx->pos) || is_newline_at(lx->pos))) {
advance(lx);
}
if (*lx->pos == '\\') {
if (lex_escline(lx, err) < 0) {
return ST_TOK_ERROR;
}
continue;
}
break;
}
if (*lx->pos == '\0') {
emit(out, ST_TOK_EOF, (struct st_span){ lx->filename, lx->line,
lx->col }, lx->pos, 0);
return ST_TOK_EOF;
}
start = (struct st_span){ lx->filename, lx->line, lx->col };
c = (unsigned char)*lx->pos;
/* numbers, signs, and dotted identifiers */
if (is_digit(c) || c == '+' || c == '-' || c == '.') {
return lex_value_start(lx, out, start, err);
}
switch (c) {
case '{':
emit(out, ST_TOK_LBRACE, start, lx->pos, 1);
advance(lx);
break;
case '}':
emit(out, ST_TOK_RBRACE, start, lx->pos, 1);
advance(lx);
break;
case ';':
emit(out, ST_TOK_SEMICOLON, start, lx->pos, 1);
advance(lx);
break;
case '=':
emit(out, ST_TOK_EQUALS, start, lx->pos, 1);
advance(lx);
break;
case '"':
if (lx->pos[1] == '"' && lx->pos[2] == '"') {
return lex_multiline_string(lx, out, start, err, false);
}
return lex_quoted_string(lx, out, start, err, false);
case '#':
return lex_hash(lx, out, start, err);
case '(':
return lex_type(lx, out, start, err);
case '/':
return lex_comment(lx, out, start, err);
default:
if (at_ident_start(lx)) {
return lex_ident(lx, out, start, err);
}
return fail(lx, start, "unexpected character in source", err);
}
return out->kind;
}
const char *
st_token_kind_name(enum st_token_kind kind)
{
switch (kind) {
case ST_TOK_EOF:
return "eof";
case ST_TOK_ERROR:
return "error";
case ST_TOK_LBRACE:
return "lbrace";
case ST_TOK_RBRACE:
return "rbrace";
case ST_TOK_SEMICOLON:
return "semicolon";
case ST_TOK_EQUALS:
return "equals";
case ST_TOK_IDENT:
return "ident";
case ST_TOK_STRING:
return "string";
case ST_TOK_MULTILINE_STRING:
return "multiline-string";
case ST_TOK_RAW_STRING:
return "raw-string";
case ST_TOK_NUMBER:
return "number";
case ST_TOK_INF:
return "inf";
case ST_TOK_NEG_INF:
return "-inf";
case ST_TOK_NAN:
return "nan";
case ST_TOK_TRUE:
return "true";
case ST_TOK_FALSE:
return "false";
case ST_TOK_NULL:
return "null";
case ST_TOK_TYPE:
return "type";
case ST_TOK_LINE_COMMENT:
return "line-comment";
case ST_TOK_BLOCK_COMMENT:
return "block-comment";
case ST_TOK_SLASHDASH:
return "slashdash";
default:
return "?";
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* lexer.h - KDL 2.0.0 tokenizer for stupidtools.
*
* Produces a FLAT token stream; it does NOT parse node/argument/property
* structure (that is the parser, todo 7). Every token carries a source
* span (struct st_span) and a borrowed text slice into the source buffer.
*
* API
* ---
* struct st_lexer *st_lexer_new(const char *src, const char *filename);
* enum st_token_kind st_lexer_next(struct st_lexer *, struct st_token *,
* struct st_error **err);
* void st_lexer_free(struct st_lexer *);
* const char *st_token_kind_name(enum st_token_kind);
*
* st_lexer_new() borrows `src` (never copies); the buffer must remain
* valid for the lexer's lifetime because token text slices point into it.
* `filename` is also borrowed and is stamped into every span.
*
* st_lexer_next() returns one token per call. It returns:
* - a token kind (>= ST_TOK_LBRACE) and fills `*out` on success,
* - ST_TOK_EOF at end of input (out->text/len are empty),
* - ST_TOK_ERROR on a lex error, in which case `*err` is set to a
* freshly allocated st_error (category ST_ERR_KDL_PARSE) and `*out`
* is left unchanged. The caller owns `*err` and frees it with
* st_error_free().
*
* ERROR-SPAN LIFETIME (borrowed span)
* ---
* st_error carries only a BORROWED span pointer (src/error.h), so the
* lexer cannot hand back a stack-local span. Instead the lexer stores the
* offending span in its own `err_span` field and passes &lx->err_span to
* st_error_at(). Consequence: the returned error's span is valid only
* until the NEXT st_lexer_next() call or st_lexer_free() — copy the span
* (or print the error) before re-lexing. This mirrors the span.h "file is
* borrowed" convention.
*
* TOKEN-ROLE NOTES (contract for todo 7, the parser)
* ---
* The lexer is deliberately role-agnostic. NODE_NAME / ARGUMENT / PROPERTY
* are parser-level roles, not lexical kinds: the parser classifies
* ST_TOK_IDENT as a node name, an argument value, or a property key
* followed by ST_TOK_EQUALS, by position. String/raw/multi-line text is
* the raw source slice (delimiters and escapes intact) — unescaping and
* multi-line dedenting belong to the value model (todo 8), not here.
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef ST_KDL_LEXER_H
#define ST_KDL_LEXER_H
#include <stddef.h>
#include "span.h"
struct st_error;
/* Token kinds, ordered so the punctuation kinds follow ST_TOK_ERROR. */
enum st_token_kind {
ST_TOK_EOF = 0, /* end of input */
ST_TOK_ERROR, /* lex error; only ever RETURNED, never stored in a token */
/* structural punctuation */
ST_TOK_LBRACE, /* { children-block open */
ST_TOK_RBRACE, /* } children-block close */
ST_TOK_SEMICOLON, /* ; node separator */
ST_TOK_EQUALS, /* = property assignment */
/* strings (KDL §3.9) */
ST_TOK_IDENT, /* bare identifier (node name / arg / property key) */
ST_TOK_STRING, /* "quoted" single-line string */
ST_TOK_MULTILINE_STRING, /* """multi-line""" string */
ST_TOK_RAW_STRING, /* #"raw"# or #"""raw multi-line"""# */
/* numbers (KDL §3.14) */
ST_TOK_NUMBER, /* decimal / 0x hex / 0o octal / 0b binary */
ST_TOK_INF, /* #inf */
ST_TOK_NEG_INF, /* #-inf */
ST_TOK_NAN, /* #nan */
/* booleans and null (KDL §3.15-3.16) */
ST_TOK_TRUE, /* #true */
ST_TOK_FALSE, /* #false */
ST_TOK_NULL, /* #null */
/* type annotation (KDL §3.8); text slice is the inner identifier */
ST_TOK_TYPE, /* (u8) -> text "u8" */
/* comments (KDL §3.17) */
ST_TOK_LINE_COMMENT, /* // ... */
ST_TOK_BLOCK_COMMENT, /* ... (possibly nested) */
ST_TOK_SLASHDASH, /* /- node/entry-level comment */
};
/* One token. `span` is the 1-based line/col of the token's first byte;
* `text`/`len` are a borrowed slice into the source (NOT NUL-terminated;
* for strings it includes the delimiters). */
struct st_token {
enum st_token_kind kind;
struct st_span span;
const char *text;
size_t len;
};
struct st_lexer;
struct st_lexer *st_lexer_new(const char *src, const char *filename);
void st_lexer_free(struct st_lexer *lx);
enum st_token_kind st_lexer_next(struct st_lexer *lx, struct st_token *out,
struct st_error **err);
/* Stable display name for a token kind (e.g. "ident"). Unknown kinds
* yield "?" rather than indexing out of range. */
const char *st_token_kind_name(enum st_token_kind kind);
#endif /* ST_KDL_LEXER_H */
+624
View File
@@ -0,0 +1,624 @@
/* LINK: ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
/* tests/unit/test_lexer.c
*
* Unit tests for the KDL 2.0.0 tokenizer (todo 6).
*
* 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). lexer.c depends on error.c (typed
* errors) and span.c (caret rendering), so all three are linked.
*
* These tests assert on REAL token streams — kind, span (line/col) and
* the text slice — never just "does not crash".
*/
#include "munit.h"
#include "error.h"
#include "kdl/lexer.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#define MAX_TOKS 256
#define MAX_TEXT 512
/* A single captured token. `text` is a NUL-terminated copy of the lexer's
* text slice (which is otherwise not NUL-terminated). */
struct captured_tok {
enum st_token_kind kind;
size_t line;
size_t col;
char text[MAX_TEXT];
};
/* A full captured stream. When the lexer hits an error, `had_error` is
* set and the error's category + span + message are copied out (values,
* not borrowed pointers) before the lexer is freed — this sidesteps the
* borrowed-span lifetime entirely inside the test. */
struct capture {
struct captured_tok toks[MAX_TOKS];
size_t n;
bool had_error;
int err_category;
size_t err_line;
size_t err_col;
char err_msg[MAX_TEXT];
};
/* Tokenize `src` to completion. On a lex error the tokens produced before
* the error are still captured (and the error info is recorded). */
static struct capture
capture(const char *src)
{
struct capture cap;
struct st_lexer *lx;
struct st_error *err = NULL;
memset(&cap, 0, sizeof cap);
lx = st_lexer_new(src, "test.kdl");
munit_assert_not_null(lx);
for (;;) {
struct st_token t;
enum st_token_kind k = st_lexer_next(lx, &t, &err);
if (k == ST_TOK_ERROR) {
cap.had_error = true;
if (err != NULL) {
cap.err_category = (int)st_error_category_of(err);
if (err->span != NULL) {
cap.err_line = err->span->line;
cap.err_col = err->span->col;
}
snprintf(cap.err_msg, sizeof cap.err_msg, "%s",
st_error_message(err));
st_error_free(err);
}
break;
}
munit_assert_size(cap.n, <, MAX_TOKS);
cap.toks[cap.n].kind = k;
cap.toks[cap.n].line = t.span.line;
cap.toks[cap.n].col = t.span.col;
{
size_t len = t.len < MAX_TEXT - 1 ? t.len : MAX_TEXT - 1;
memcpy(cap.toks[cap.n].text, t.text, len);
cap.toks[cap.n].text[len] = '\0';
}
cap.n++;
if (k == ST_TOK_EOF) {
break;
}
}
st_lexer_free(lx);
return cap;
}
/* Assert token i is (kind, line, col, text). */
static void
assert_tok(const struct capture *cap, size_t i, enum st_token_kind kind,
size_t line, size_t col, const char *text)
{
munit_assert_size(i, <, cap->n);
munit_assert_int(cap->toks[i].kind, ==, kind);
munit_assert_size(cap->toks[i].line, ==, line);
munit_assert_size(cap->toks[i].col, ==, col);
munit_assert_string_equal(cap->toks[i].text, text);
}
/* Assert a lex error occurred with the given (line, col) span. */
static void
assert_error(const struct capture *cap, size_t line, size_t col)
{
munit_assert_true(cap->had_error);
munit_assert_int(cap->err_category, ==, ST_ERR_KDL_PARSE);
munit_assert_size(cap->err_line, ==, line);
munit_assert_size(cap->err_col, ==, col);
}
/* --- acceptance: the plan's canonical node ----------------------------- */
static MunitResult
test_acceptance_node(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("node 1 2 \"x\" key=\"v\" { child }");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "node");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 6, "1");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 8, "2");
assert_tok(&cap, 3, ST_TOK_STRING, 1, 10, "\"x\"");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 14, "key");
assert_tok(&cap, 5, ST_TOK_EQUALS, 1, 17, "=");
assert_tok(&cap, 6, ST_TOK_STRING, 1, 18, "\"v\"");
assert_tok(&cap, 7, ST_TOK_LBRACE, 1, 22, "{");
assert_tok(&cap, 8, ST_TOK_IDENT, 1, 24, "child");
assert_tok(&cap, 9, ST_TOK_RBRACE, 1, 30, "}");
assert_tok(&cap, 10, ST_TOK_EOF, 1, 31, "");
munit_assert_size(cap.n, ==, 11);
return MUNIT_OK;
}
/* --- numbers: every radix + sign/float/exponent/underscore ------------ */
static MunitResult
test_numbers(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0x10 0o17 0b101 123 3.14 1e10 1_000");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_NUMBER, 1, 1, "0x10");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 6, "0o17");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 11, "0b101");
assert_tok(&cap, 3, ST_TOK_NUMBER, 1, 17, "123");
assert_tok(&cap, 4, ST_TOK_NUMBER, 1, 21, "3.14");
assert_tok(&cap, 5, ST_TOK_NUMBER, 1, 26, "1e10");
assert_tok(&cap, 6, ST_TOK_NUMBER, 1, 31, "1_000");
assert_tok(&cap, 7, ST_TOK_EOF, 1, 36, "");
return MUNIT_OK;
}
static MunitResult
test_signed_and_radix_numbers(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("-5 +7 -0x10 0XFF 0xff 1e-3");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_NUMBER, 1, 1, "-5");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 4, "+7");
assert_tok(&cap, 2, ST_TOK_NUMBER, 1, 7, "-0x10");
assert_tok(&cap, 3, ST_TOK_NUMBER, 1, 13, "0XFF");
assert_tok(&cap, 4, ST_TOK_NUMBER, 1, 18, "0xff");
assert_tok(&cap, 5, ST_TOK_NUMBER, 1, 23, "1e-3");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 27, "");
return MUNIT_OK;
}
/* --- keyword numbers, booleans, null ---------------------------------- */
static MunitResult
test_keywords(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#inf #-inf #nan #true #false #null");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_INF, 1, 1, "#inf");
assert_tok(&cap, 1, ST_TOK_NEG_INF, 1, 6, "#-inf");
assert_tok(&cap, 2, ST_TOK_NAN, 1, 12, "#nan");
assert_tok(&cap, 3, ST_TOK_TRUE, 1, 17, "#true");
assert_tok(&cap, 4, ST_TOK_FALSE, 1, 23, "#false");
assert_tok(&cap, 5, ST_TOK_NULL, 1, 30, "#null");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 35, "");
return MUNIT_OK;
}
/* --- type annotations -------------------------------------------------- */
static MunitResult
test_type_annotation(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("(u8)42 (published)date");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_TYPE, 1, 1, "u8");
assert_tok(&cap, 1, ST_TOK_NUMBER, 1, 5, "42");
assert_tok(&cap, 2, ST_TOK_TYPE, 1, 8, "published");
assert_tok(&cap, 3, ST_TOK_IDENT, 1, 19, "date");
assert_tok(&cap, 4, ST_TOK_EOF, 1, 23, "");
return MUNIT_OK;
}
/* --- string forms: quoted, raw (single + double hash) ------------------ */
static MunitResult
test_strings_quoted_raw(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"hello\" #\"raw\"# ##\"raw2\"##");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_STRING, 1, 1, "\"hello\"");
assert_tok(&cap, 1, ST_TOK_RAW_STRING, 1, 9, "#\"raw\"#");
assert_tok(&cap, 2, ST_TOK_RAW_STRING, 1, 17, "##\"raw2\"##");
assert_tok(&cap, 3, ST_TOK_EOF, 1, 27, "");
return MUNIT_OK;
}
/* --- multi-line string (delimiters only; dedent rules are todo 8) ------ */
static MunitResult
test_multiline_string(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\"\"multi\"\"\"");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_MULTILINE_STRING, 1, 1, "\"\"\"multi\"\"\"");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 12, "");
return MUNIT_OK;
}
/* --- comments ---------------------------------------------------------- */
static MunitResult
test_line_comment(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a // comment\nb");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_LINE_COMMENT, 1, 3, "// comment");
assert_tok(&cap, 2, ST_TOK_IDENT, 2, 1, "b");
assert_tok(&cap, 3, ST_TOK_EOF, 2, 2, "");
return MUNIT_OK;
}
static MunitResult
test_block_comment_nested(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a /* x /* n */ */ b");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_BLOCK_COMMENT, 1, 3, "/* x /* n */ */");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 19, "b");
assert_tok(&cap, 3, ST_TOK_EOF, 1, 20, "");
return MUNIT_OK;
}
static MunitResult
test_slashdash(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("/- node");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_SLASHDASH, 1, 1, "/-");
assert_tok(&cap, 1, ST_TOK_IDENT, 1, 4, "node");
assert_tok(&cap, 2, ST_TOK_EOF, 1, 8, "");
return MUNIT_OK;
}
/* --- structural tokens -------------------------------------------------- */
static MunitResult
test_semicolons_braces(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("a;b {c}");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "a");
assert_tok(&cap, 1, ST_TOK_SEMICOLON, 1, 2, ";");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 3, "b");
assert_tok(&cap, 3, ST_TOK_LBRACE, 1, 5, "{");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 6, "c");
assert_tok(&cap, 5, ST_TOK_RBRACE, 1, 7, "}");
assert_tok(&cap, 6, ST_TOK_EOF, 1, 8, "");
return MUNIT_OK;
}
/* --- identifiers: sign / dotted / hyphen forms ------------------------- */
static MunitResult
test_ident_specials(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("--this .md foo-bar a+b _under");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "--this");
assert_tok(&cap, 1, ST_TOK_IDENT, 1, 8, ".md");
assert_tok(&cap, 2, ST_TOK_IDENT, 1, 12, "foo-bar");
assert_tok(&cap, 3, ST_TOK_IDENT, 1, 20, "a+b");
assert_tok(&cap, 4, ST_TOK_IDENT, 1, 24, "_under");
assert_tok(&cap, 5, ST_TOK_EOF, 1, 30, "");
return MUNIT_OK;
}
/* --- bare keyword identifiers are syntax errors ------------------------ */
static MunitResult
test_bare_keywords_error(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
static const char *const bare[] = {
"inf", "-inf", "nan", "true", "false", "null",
};
size_t i;
for (i = 0; i < sizeof(bare) / sizeof(bare[0]); i++) {
struct capture cap = capture(bare[i]);
assert_error(&cap, 1, 1);
}
/* also rejected in position, with the span on the offending ident */
{
struct capture cap = capture("x inf");
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "x");
assert_error(&cap, 1, 3);
}
return MUNIT_OK;
}
/* --- unterminated strings (all three forms) ---------------------------- */
static MunitResult
test_unterminated_quoted(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("node \"unterminated");
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "node");
assert_error(&cap, 1, 6); /* span at the opening quote */
return MUNIT_OK;
}
static MunitResult
test_unterminated_raw(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#\"unterminated");
assert_error(&cap, 1, 1); /* span at the opening '#' */
return MUNIT_OK;
}
static MunitResult
test_unterminated_multiline(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\"\"multi");
assert_error(&cap, 1, 1); /* span at the opening '\"\"\"' */
return MUNIT_OK;
}
/* --- malformed numbers -------------------------------------------------- */
static MunitResult
test_bad_hex(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0xZZ");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
static MunitResult
test_hex_no_digits(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0x");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
static MunitResult
test_bad_octal_binary(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("0o8");
assert_error(&cap, 1, 1);
cap = capture("0b2");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- leading-dot numbers are errors ------------------------------------ */
static MunitResult
test_dot_number_error(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture(".5");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- invalid escape ----------------------------------------------------- */
static MunitResult
test_invalid_escape(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("\"\\q\"");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- stray '#' ---------------------------------------------------------- */
static MunitResult
test_hash_alone(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("#");
assert_error(&cap, 1, 1);
cap = capture("##foo");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- bad type annotation ------------------------------------------------ */
static MunitResult
test_bad_type_annotation(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("(u8");
assert_error(&cap, 1, 1);
return MUNIT_OK;
}
/* --- a stray '}' is a token, not a lexer error (nesting is todo 7) ----- */
static MunitResult
test_stray_brace_is_token(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("}");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_RBRACE, 1, 1, "}");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 2, "");
return MUNIT_OK;
}
/* --- hostile span: column well past 60 stays exact --------------------- */
static MunitResult
test_long_line_span(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
char src[256];
memset(src, ' ', 70);
memcpy(src + 70, "foo", 4);
{
struct capture cap = capture(src);
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 71, "foo");
assert_tok(&cap, 1, ST_TOK_EOF, 1, 74, "");
}
return MUNIT_OK;
}
/* --- line/col tracking across newlines --------------------------------- */
static MunitResult
test_line_col_tracking(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct capture cap = capture("foo\n bar");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_IDENT, 1, 1, "foo");
assert_tok(&cap, 1, ST_TOK_IDENT, 2, 3, "bar");
assert_tok(&cap, 2, ST_TOK_EOF, 2, 6, "");
/* a multi-line string advances line/col for following tokens */
cap = capture("\"\"\"\nx\n\"\"\" tail");
munit_assert_false(cap.had_error);
assert_tok(&cap, 0, ST_TOK_MULTILINE_STRING, 1, 1, "\"\"\"\nx\n\"\"\"");
assert_tok(&cap, 1, ST_TOK_IDENT, 3, 5, "tail");
assert_tok(&cap, 2, ST_TOK_EOF, 3, 9, "");
return MUNIT_OK;
}
/* --- token kind names exist and are distinct --------------------------- */
static MunitResult
test_kind_names(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
const char *prev = NULL;
int k;
for (k = ST_TOK_EOF; k <= ST_TOK_SLASHDASH; k++) {
const char *n = st_token_kind_name((enum st_token_kind)k);
munit_assert_not_null(n);
munit_assert_int((int)strlen(n), >, 0);
if (prev != NULL) {
munit_assert_string_not_equal(n, prev);
}
prev = n;
}
return MUNIT_OK;
}
static MunitTest tests[] = {
{ "/lexer/acceptance-node", test_acceptance_node, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/numbers", test_numbers, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/signed-radix-numbers", test_signed_and_radix_numbers, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/keywords", test_keywords, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/type-annotation", test_type_annotation, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/strings-quoted-raw", test_strings_quoted_raw, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/multiline-string", test_multiline_string, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/line-comment", test_line_comment, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/block-comment-nested", test_block_comment_nested, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/slashdash", test_slashdash, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/semicolons-braces", test_semicolons_braces, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/ident-specials", test_ident_specials, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bare-keywords-error", test_bare_keywords_error, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-quoted", test_unterminated_quoted, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-raw", test_unterminated_raw, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/unterminated-multiline", test_unterminated_multiline, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-hex", test_bad_hex, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/hex-no-digits", test_hex_no_digits, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-octal-binary", test_bad_octal_binary, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/dot-number-error", test_dot_number_error, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/invalid-escape", test_invalid_escape, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/hash-alone", test_hash_alone, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/bad-type-annotation", test_bad_type_annotation, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/stray-brace-is-token", test_stray_brace_is_token, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/long-line-span", test_long_line_span, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/line-col-tracking", test_line_col_tracking, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/lexer/kind-names", test_kind_names, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
};
static const MunitSuite suite = {
"/lexer", 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);
}