tools: Extract enum parsing to more generic libenum

This commit is contained in:
Rachel
2026-04-20 22:25:35 -07:00
parent 41858fc6bd
commit 54e7939d73
12 changed files with 1201 additions and 367 deletions

View File

@@ -34,7 +34,7 @@ dataproc_templates_dir = meson.current_source_dir() / 'data'
commonproc_dep = declare_dependency(
link_with: static_library(
'commonproc',
sources: files('src/common.c', 'src/enum.c'),
sources: files('src/common.c'),
c_args: [
dataproc_cflags,
@@ -43,7 +43,12 @@ commonproc_dep = declare_dependency(
f'-DTEMPLATES_DIR="@dataproc_templates_dir@"',
],
dependencies: [ dataproc_dep, nitroarc_dep ],
dependencies: [
dataproc_dep,
nitroarc_dep,
libenum_dep,
libexpr_dep,
],
native: true,
),
dependencies: [ dataproc_dep, nitroarc_dep ],

View File

@@ -10,11 +10,19 @@
#include <unistd.h>
#include "dataproc.h"
#include "enum.h"
#include "libenum.h"
#include "libexpr.h"
#include "nitroarc.h"
#define MAX_LOADED_ENUMS 128
typedef struct enum_t enum_t;
struct enum_t {
lookup_t *members;
size_t size;
char *pool;
};
static enum_t loaded_enums[MAX_LOADED_ENUMS] = { 0 };
static size_t num_loaded_enums = 0;
@@ -194,7 +202,7 @@ static void load_header_template(header_template_t *h, FILE *depfile) {
memcpy(template_fname + len_out_fname, HEADER_TEMPLATE_SUFFIX, sizeof(HEADER_TEMPLATE_SUFFIX));
char *full_path = pathjoin(TEMPLATES_DIR, NULL, template_fname);
char *template = fload(full_path);
char *template = fload(full_path, NULL);
char *marker = strstr(template, HEADER_TEMPLATE_MAGIC);
char *footer = marker + sizeof(HEADER_TEMPLATE_MAGIC);
*marker = '\0';
@@ -210,7 +218,10 @@ static void load_header_template(header_template_t *h, FILE *depfile) {
}
static void unload_enums(void) {
for (size_t i = 0; i < num_loaded_enums; i++) enum_free(&loaded_enums[i]);
for (size_t i = 0; i < num_loaded_enums; i++) {
free(loaded_enums[i].pool);
free(loaded_enums[i].members);
}
}
static void finish_outputs(void) {
@@ -364,11 +375,12 @@ void splitenv(const char *name, char ***target, size_t *target_len, const char *
}
}
char* fload(const char *filename) {
char* fload(const char *filename, size_t *out_size) {
char *buf = NULL;
FILE *f = fopen(filename, "rb");
if (f == NULL) {
fprintf(stderr, "could not open file '%s': %s\n", filename, strerror(errno));
if (*out_size) *out_size = 0;
return NULL;
}
@@ -390,6 +402,7 @@ char* fload(const char *filename) {
buf[fsize] = 0;
cleanup:
if (out_size) *out_size = fsize;
fclose(f);
return buf;
}
@@ -463,6 +476,21 @@ static const char *include_paths[MAX_INCLUDES] = {
REPO_BUILD,
};
static int membercmp(const void *lhs, const void *rhs) {
const lookup_t *l = lhs;
const lookup_t *r = rhs;
if (l == NULL && r == NULL) return 0;
else if (l == NULL) return 1;
else if (r == NULL) return -1;
if (l->def == NULL && r->def == NULL) return 0;
else if (l->def == NULL) return 1;
else if (r->def == NULL) return -1;
return strcmp(l->def, r->def);
}
static enum_t dp_include(
const char *from_file,
const char *with_prefix,
@@ -471,6 +499,9 @@ static enum_t dp_include(
FILE *depfile
) {
assert(from_file && "included filename must not be NULL");
if (!from_defs) {
assert(strncmp("enum", for_type, sizeof("enum") - 1) == 0 && "'for_type' value must be prefixed with 'enum '");
}
char *found_file = NULL;
for (int i = 0; i < MAX_INCLUDES && include_paths[i]; i++) {
@@ -484,16 +515,55 @@ static enum_t dp_include(
exit(EXIT_FAILURE);
}
char *buf = fload(found_file);
enum_t result = from_defs
? enum_parse_def(buf, with_prefix, ENUM_F_SORT | ENUM_F_CONVERT)
: enum_parse_one(buf, ENUM_F_SORT | ENUM_F_CONVERT, NULL);
size_t bufsize = 0;
char *endptr = NULL;
char *buf = fload(found_file, &bufsize);
enum_seq_t parsed = from_defs
? libenum_loadcpp(buf, bufsize, with_prefix, &endptr)
: libenum_find(buf, bufsize, &for_type[5], &endptr); // strip 'enum ' prefix
dp_register((lookup_t *)result.syms, result.len, for_type);
if (parsed.errc != LIBENUM_E_OK) {
// TODO: line number + column number of the error
fprintf(stderr, "syntax error while parsing included file '%s': %s\n",
found_file, libenum_errs(parsed.errc));
exit(EXIT_FAILURE);
}
else if (parsed.members == NULL) {
fprintf(stderr, "enum named '%s' could not be found in file '%s'\n",
for_type, found_file);
exit(EXIT_FAILURE);
}
enum_t result = {
.members = calloc(parsed.size, sizeof(*result.members)),
.size = parsed.size,
.pool = parsed.pool,
};
long curr = 0;
for (size_t i = 0; i < parsed.size; i++) {
result.members[i].def = parsed.members[i].name;
if (parsed.members[i].expr == NULL) {
result.members[i].val = curr++;
}
else {
// WARN: this cast is filthy, but it works!
result.members[i].val = libexpr_eval(parsed.members[i].expr, &endptr, (scope_t *)&result);
curr = result.members[i].val + 1;
if (*endptr) {
fprintf(stderr, "syntax error while parsing expression '%s' from included file '%s'\n",
parsed.members[i].expr, found_file);
exit(EXIT_FAILURE);
}
}
}
qsort(result.members, result.size, sizeof(*result.members), membercmp);
dp_register(result.members, result.size, for_type);
fputs(found_file, depfile);
fputc(' ', depfile);
free(buf);
free(found_file);
return result;
}

View File

@@ -126,7 +126,7 @@ char* strupper(const char *s);
char* strjoin(const char *s, const char *with, const char *sep);
void splitenv(const char *name, char ***target, size_t *target_len, const char **extra, size_t extra_len);
char* fload(const char *filename);
char* fload(const char *filename, size_t *out_size);
char* pathjoin(const char *basedir, const char *subdir, const char *file);
char* guardify(const char *path);

View File

@@ -1,273 +0,0 @@
#include "enum.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Duplicate a string onto the heap.
static char* strdup_(const char *s) {
size_t l = strlen(s);
char *d = calloc(l + 1, sizeof(*d));
memcpy(d, s, l);
return d;
}
// Find the first occurrence of `c` (or `\0`) within `s`.
static char* strchrnul_(const char *s, int c) {
while (s && *s && *s != c) s++;
return (char *)s;
}
// Find the first occurrence of a character OTHER than `c` (or `\0`) within `s`.
static char* strnchr(const char *s, int c) {
while (s && *s && *s == c) s++;
return (char *)s;
}
// Locates the first occurrence of `c` (or `\0`) within `*s` and replaces it
// with `\0`. `*s` is then advanced to the succeeding character, and the
// original value of `*s` is returned.
//
// If `*s` is NULL, then NULL is returned immediately.
static char* strcsep(char **s, char c) {
char *p = *s;
char *e = strchrnul_(p, c);
if (*e) *e++ = 0; else e = 0;
*s = e;
return p;
}
// Locates the first occurrence of any character from `charset` within `*s` and
// replaces it with `\0`. `*s` is then advanced to the succeeding character, and
// the original value of `*s` is returned.
//
// If `*s` is NULL, then NULL is returned immediately.
//
// Generalization of `strcsep` for multiple-characters.
static char* strssep(char **s, const char *charset) {
char *p = *s;
if (!p) return NULL;
char *e = p + strcspn(p, charset);
if (*e) *e++ = 0; else e = 0;
*s = e;
return p;
}
#define INIT_CAP 256
static int push_symval(enum_t *table, const char *tok, const char *val, unsigned flags) {
if (table->len + 1 >= table->cap) {
size_t new_cap = table->cap * 3 / 2;
symb_t *new_sym = realloc(table->syms, new_cap * sizeof(*table->syms));
if (new_sym == NULL) return 1;
table->syms = new_sym;
table->cap = new_cap;
}
if (flags & ENUM_F_CONVERT) { // if val == NULL, get previous (syms[len-1]) and increment
if (val == NULL) {
const symb_t *prev_sym = &table->syms[table->len - 1];
table->syms[table->len++] = (symb_t){
.tok = (char *)tok,
.val_int = prev_sym->val_int + 1,
};
}
else {
table->syms[table->len++] = (symb_t){
.tok = (char *)tok,
.val_int = strtol(val, NULL, 0),
};
}
}
else {
table->syms[table->len++] = (symb_t){
.tok = (char *)tok,
.val_lit = (char *)val
};
}
return 0;
}
static int compare_tok(const void *lhs, const void *rhs) {
const symb_t *sym_lhs = lhs;
const symb_t *sym_rhs = rhs;
return strcmp(sym_lhs->tok, sym_rhs->tok);
}
static int push_define(enum_t *table, char **s, unsigned flags) {
char *sym = strcsep(s, ' ');
char *val = strnchr(*s, ' ');
return (val && *val) ? push_symval(table, sym, val, flags) : 0;
}
static void push_enum_member(enum_t *table, char **s, unsigned flags) {
char *line = strnchr(strcsep(s, '\n'), ' ');
char *sym = strssep(&line, " ,");
char *equ = strnchr(line, ' ');
char *val = equ && *equ == '=' ? strnchr(equ + 1, ' ') : NULL;
char *end = strchrnul_(val, ',');
if (end) *end = 0;
if (*sym) push_symval(table, sym, val, flags);
}
enum_t enum_parse_def(const char *buf, const char *prefix, unsigned flags) {
enum_t ret = {
.name = prefix ? (char *)prefix : NULL,
.pool = strdup_(buf),
.syms = calloc(INIT_CAP, sizeof(*ret.syms)),
.cnv = !!(flags & ENUM_F_CONVERT),
.len = 0,
.cap = INIT_CAP,
};
char *s = ret.pool;
size_t len = prefix ? strlen(prefix) : 0;
while (*s) {
char *line = strcsep(&s, '\n');
if (strncmp(line, "#define ", 8) != 0) continue; else line += 8;
if (prefix && strncmp(line, prefix, len) != 0) continue;
if (push_define(&ret, &line, flags) != 0) break;
}
if (flags & ENUM_F_SORT) qsort(ret.syms, ret.len, sizeof(*ret.syms), compare_tok);
return ret;
}
enum_t enum_parse_one(const char *buf, unsigned flags, char **endptr) {
enum_t ret = {
.name = NULL,
.pool = strdup_(buf),
.syms = calloc(INIT_CAP, sizeof(*ret.syms)),
.cnv = !!(flags & ENUM_F_CONVERT),
.len = 0,
.cap = INIT_CAP,
};
char *s = ret.pool;
while (*s) {
char *line = strcsep(&s, '\n');
if (strncmp(line, "enum ", 5) != 0) continue; else line += 5;
if (*line != '{') {
ret.name = strssep(&line, " {");
}
// NOTE: assumption: opening brace is on the same line, and members
// are defined one-per-line
while (*s && strncmp(s, "};", 2) != 0) {
push_enum_member(&ret, &s, flags);
}
s += 2;
break; // stop after the one `enum` is found
}
if (flags & ENUM_F_SORT) qsort(ret.syms, ret.len, sizeof(*ret.syms), compare_tok);
if (endptr) *endptr = (char *)buf + (s - ret.pool);
return ret;
}
static enum_t* push_subtable(enums_t *tables, const char *name, unsigned flags) {
if (tables->len + 1 >= tables->cap) {
size_t new_cap = tables->cap * 3 / 2;
enum_t *new_arr = realloc(tables->enums, new_cap * sizeof(*tables->enums));
if (new_arr == NULL) return NULL;
tables->enums = new_arr;
tables->cap = new_cap;
}
enum_t *next = &tables->enums[tables->len++];
next->name = (char *)name;
next->pool = NULL;
next->syms = calloc(INIT_CAP, sizeof(*next->syms));
next->cnv = !!(flags & ENUM_F_CONVERT);
next->len = 0;
next->cap = INIT_CAP;
return next;
}
static int compare_table(const void *lhs, const void *rhs) {
const enum_t *table_lhs = lhs;
const enum_t *table_rhs = rhs;
return strcmp(table_lhs->name, table_rhs->name);
}
enums_t enum_parse_all(const char *buf, unsigned flags) {
enums_t ret = {
.enums = calloc(INIT_CAP, sizeof(*ret.enums)),
.pool = strdup_(buf),
.len = 0,
.cap = INIT_CAP,
};
push_subtable(&ret, NULL, flags);
char *s = ret.pool;
while (*s) {
char *line = strcsep(&s, '\n');
enum_t *sub = &ret.enums[0];
if (strncmp(line, "enum ", 5) == 0) {
line += 5;
if (*line != '{') {
sub = push_subtable(&ret, strssep(&line, " {"), flags);
if (sub == NULL) break;
}
while (*s && strncmp(s, "};", 2) != 0) {
push_enum_member(sub, &s, flags);
}
}
else if (strncmp(line, "#define ", 8) == 0) {
line += 8;
push_define(sub, &line, flags);
}
else continue;
}
if (flags & ENUM_F_SORT) {
for (size_t i = 0; i < ret.len; i++) {
qsort(ret.enums[i].syms,
ret.enums[i].len,
sizeof(*ret.enums[i].syms),
compare_tok);
}
qsort(ret.enums + 1, ret.len - 1, sizeof(*ret.enums), compare_table);
}
return ret;
}
void enum_free(enum_t *table) {
free(table->pool);
free(table->syms);
table->name = NULL;
table->pool = NULL;
table->syms = NULL;
}
void enum_free_all(enums_t *tables) {
for (size_t i = 0; i < tables->len; i++) enum_free(&tables->enums[i]);
free(tables->enums);
free(tables->pool);
tables->enums = NULL;
tables->pool = NULL;
}

View File

@@ -1,78 +0,0 @@
#ifndef ENUM_H
#define ENUM_H
#include <stdbool.h>
#include <stddef.h>
typedef struct symb_t symb_t;
struct symb_t {
union {
char *val_lit;
long val_int;
};
char *tok;
};
typedef struct enum_t enum_t;
struct enum_t {
char *name;
char *pool;
symb_t *syms;
bool cnv;
size_t len;
size_t cap;
};
typedef struct enums_t enums_t;
struct enums_t {
enum_t *enums;
char *pool;
size_t len;
size_t cap;
};
#define ENUM_F_SORT (1 << 0)
#define ENUM_F_CONVERT (1 << 1)
// Parse a C file `buf` for defined preprocesor tokens with replacement values
// and return them as a symbol-table. If `prefix` is given as non-`NULL`, it
// will be used to filter preprocessor tokens from `buf` that are included in
// the output.
//
// If `flags` contains `ENUM_F_SORT`, then symbols in the output table will be
// sorted lexicographically by their tokens.
//
// If `flags` contains `ENUM_F_CONVERT`, then numeric strings will be parsed
// into integer values.
enum_t enum_parse_def(const char *buf, const char *prefix, unsigned flags);
// Parse a C file `buf` for a named `enum` and return its member-names as a
// symbol-table. If `e_name` is given as `NULL`, then this routine will return
// an empty symbol-table; otherwise, its value is used to find a matching `enum`
// from `buf` whose member-names shall be loaded into the output.
//
// If `flags` contains `ENUM_F_SORT`, then symbols in the output table will be
// sorted lexicographically by their tokens.
//
// If `flags` contains `ENUM_F_CONVERT`, then numeric strings will be parsed
// into integer values.
//
// If `endptr` is not `NULL`, then `*endptr` will be set to the next unprocessed
// character in `buf` upon exit.
enum_t enum_parse_one(const char *buf, unsigned flags, char **endptr);
// Parse a C file `buf` for both `enum`s and defined preprocessor tokens and
// return them as a table of symbol-tables. All preprocessor tokens and members
// of unnamed `enum`s will be present in the symbol-table at `enums[0]`; members
// of named `enum`s will be present in their own individual symbol-tables.
//
// If `flags` contains `ENUM_F_SORT`, then symbols in the output tables will be
// sorted lexicographically by their tokens, and all named output tables wiil be
// sorted lexicographically by their names.
enums_t enum_parse_all(const char *buf, unsigned flags);
// Free allocations in a symbol-table.
void enum_free(enum_t *table);
void enum_free_all(enums_t *tables);
#endif // ENUM_H

View File

@@ -311,6 +311,27 @@ datagen_cpp_commands = [
for file in (homedir / "tools" / "datagen").rglob("*.cpp")
]
enumproc_c_commands = [
{
"directory": builddir,
"arguments": [
"gcc",
f"-I{homedir}/tools/enumproc",
"-std=gnu17",
"-Wall",
"-Wextra",
"-Wpedantic",
"-Wconversion",
"-Wno-sign-conversion",
"-o",
file.with_suffix(".o"),
file.resolve(),
],
"file": file.resolve(),
}
for file in (homedir / "tools" / "enumproc").rglob("*.c")
]
dataproc_c_commands = [
{
"directory": builddir,
@@ -318,6 +339,7 @@ dataproc_c_commands = [
"gcc",
f"-I{homedir}/subprojects/yyjson-0.12.0/src",
f"-I{homedir}/tools/nitroarc/lib/include",
f"-I{homedir}/tools/enumproc",
f"-I{homedir}/tools/dataproc/lib/include",
f"-I{homedir}/include",
f"-I{builddir}",

509
tools/enumproc/libenum.c Normal file
View File

@@ -0,0 +1,509 @@
/*
* #include <libenum.h> - A library for loading C enums into member-mappings
* Copyright (C) 2026 <rachel@lhea.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "libenum.h"
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
typedef struct enum_lex enum_lex_t;
struct enum_lex {
const char *p; // cursor location
const char *e; // end-marker
const char *s; // pointer to the start of the token
size_t n; // size of the token
};
static unsigned init(enum_seq_t *seq, size_t size);
static int lex(enum_lex_t *lexer);
static char* skip(const char *str, int (*is_space)(char c));
static inline int is_hspace(char c) {
return c == ' ' || c == '\t';
}
static inline int is_vspace(char c) {
return c == '\r' || c == '\n';
}
static inline int is_wspace(char c) {
return is_hspace(c) || is_vspace(c);
}
static inline int is_alpha(char c) {
return ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'));
}
static inline int is_digit(char c) {
return (c >= '0') && (c <= '9');
}
static inline int is_word(char c) {
return is_alpha(c) || is_digit(c) || c == '_';
}
enum {
T_EOS,
T_RBRACE,
T_KWENUM,
T_LBRACE,
T_COMMA,
T_EQUALS,
T_IDENTIFIER,
T_EXPRESSION,
T_INVALID = -1,
T_INVALIDKW = -2,
};
#define return_error(code) do { result.errc = code; goto error; } while (0)
#define EXPECT_LBRACE (1 << 0)
#define EXPECT_COMMA (1 << 1)
#define EXPECT_EQUALS (1 << 2)
#define EXPECT_NAME (1 << 3)
#define EXPECT_MEMBER (1 << 4)
static inline char* poolpush(const char **target, char *dst, const char *src, size_t n) {
assert(target);
assert(dst);
assert(src);
*target = memcpy(dst, src, n);
dst[n] = 0;
return dst + n + 1;
}
enum_seq_t libenum_load(const char *str, size_t size, char **endptr) {
enum_lex_t lexer = {
.p = str,
.e = str + size,
.s = NULL,
.n = 0,
};
enum_seq_t result = { 0 };
if (init(&result, size) != LIBENUM_E_OK) return_error(result.errc);
char *p_pool = result.pool;
int token = 0;
unsigned expect = 0;
size_t cap = 256;
while ((token = lex(&lexer)) > T_RBRACE) {
switch (token) {
case T_KWENUM:
// `enum` is only valid at the start of the stream.
if (result.members) return_error(LIBENUM_E_KEYWORD);
result.members = calloc(cap, sizeof(*result.members));
if (!result.members) return_error(LIBENUM_E_ALLOC);
expect = EXPECT_LBRACE | EXPECT_NAME;
break;
case T_LBRACE:
if ((expect & EXPECT_LBRACE) == 0) return_error(LIBENUM_E_TOKEN);
expect = EXPECT_MEMBER;
break;
case T_COMMA:
if ((expect & EXPECT_COMMA) == 0) return_error(LIBENUM_E_TOKEN);
expect = EXPECT_MEMBER;
result.size++;
break;
case T_EQUALS:
if ((expect & EXPECT_EQUALS) == 0) return_error(LIBENUM_E_TOKEN);
lexer.p = skip(lexer.p, is_wspace);
lexer.s = lexer.p;
for (
lexer.p = skip(lexer.p, is_wspace), lexer.s = lexer.p;
lexer.p < lexer.e && *lexer.p && *lexer.p != ',' && *lexer.p != '}';
lexer.p++
);
p_pool = poolpush(&result.members[result.size].expr, p_pool,
lexer.s, lexer.p - lexer.s);
expect = EXPECT_COMMA;
break;
case T_IDENTIFIER:
if ((expect & EXPECT_NAME)) {
p_pool = poolpush(&result.name, p_pool, lexer.s, lexer.n);
expect = EXPECT_LBRACE;
break;
}
if ((expect & EXPECT_MEMBER) == 0) return_error(LIBENUM_E_STRAYMEMB);
if (result.size + 1 >= cap) {
const size_t new = cap * 2;
const size_t size = new * sizeof(*result.members);
enum_member_t *tmp = realloc(result.members, size);
if (!tmp) return_error(LIBENUM_E_ALLOC);
memset(&tmp[result.size], 0, cap * sizeof(*tmp));
result.members = tmp;
cap = new;
}
for (size_t i = 0; i < result.size; i++) {
if (strncmp(result.members[i].name, lexer.s, lexer.n) == 0
&& strlen(result.members[i].name) == lexer.n) {
return_error(LIBENUM_E_DUPLICATE);
}
}
p_pool = poolpush(&result.members[result.size].name, p_pool,
lexer.s, lexer.n);
expect = EXPECT_EQUALS | EXPECT_COMMA;
break;
default: assert(0 && "unexpected token type");
}
}
switch (token) {
case T_EOS: return_error(LIBENUM_E_EOS);
case T_INVALID: return_error(LIBENUM_E_TOKEN);
case T_INVALIDKW: return_error(LIBENUM_E_KEYWORD);
case T_RBRACE:
// Account for any member followed by a closing-brace
if (expect & EXPECT_COMMA) result.size++;
break;
}
if (result.size == 0) return_error(LIBENUM_E_EMPTY);
lexer.p = skip(lexer.p, is_wspace);
if (lexer.p >= lexer.e || *lexer.p != ';') return_error(LIBENUM_E_SEMICOLON);
if (endptr) *endptr = (char *)lexer.p;
return result;
error:
free(result.members);
free(result.pool);
if (endptr) *endptr = (char *)(lexer.s ? lexer.s : lexer.p);
result.members = NULL;
result.pool = NULL;
result.name = NULL;
result.size = 0;
return result;
}
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define lengthof(s) (countof(s) - 1)
enum_seq_t libenum_find(const char *str, size_t size, const char *name,
char **endptr) {
const char *p = str;
const char *e = str + size;
// skip over tokens / lines until we find an enum keyword which is followed
// by an identifier matching 'name'
for (; p < e; p++) {
if (*p == '#') { // scan to end-of-line, then continue
do { p++; } while (*p != '\n');
continue;
}
if (!is_alpha(*p)) continue;
const char *s = p;
p = skip(p, is_wspace);
if ((size_t)(e - p) < lengthof("enum ")) continue; // too short
if (strncmp(p, "enum", lengthof("enum")) != 0) continue; // not "enum"
if (!is_wspace(p[lengthof("enum")])) continue; // unnamed
p += lengthof("enum");
const char *sym_beg = skip(p, is_wspace);
if (sym_beg < e && !is_alpha(*sym_beg) && *sym_beg != '_') {
p = sym_beg;
continue;
}
const char *sym_end = sym_beg;
do { sym_end++; } while (sym_end < e && is_word(*sym_end));
if (strncmp(name, sym_beg, sym_end - sym_beg) != 0) {
p = sym_end;
continue;
}
return libenum_load(s, e - s, endptr);
}
if (endptr) *endptr = (char *)p;
return (enum_seq_t){ 0 };
}
// reference: https://cppreference.com/c/keyword
static const char *keywords[] = {
"auto",
"break",
"case",
"char",
"const",
"continue",
"default",
"do",
"double",
"else",
// "enum" is allowed for obvious reasons
"extern",
"float",
"for",
"goto",
"if",
"int",
"long",
"register",
"return",
"short",
"signed",
"sizeof",
"static",
"struct",
"switch",
"typedef",
"union",
"unsigned",
"void",
"volatile",
"while",
#if __STDC_VERSION__ >= 199901L
"inline",
"restrict",
"_Bool",
"_Complex",
"_Imaginary",
#endif
#if __STDC_VERSION__ >= 201112L
"_Alignas",
"_Alignof",
"_Atomic",
"_Generic",
"_Noreturn",
"_Static_assert",
"_Thread_local",
#endif
#if __STDC_VERSION__ >= 202311L
"alignas",
"alignof",
"bool",
"constexpr",
"false",
"nullptr",
"static_assert",
"thread_local",
"true",
"typeof",
"typeof_unqual",
"_BitInt",
"_Decimal128",
"_Decimal32",
"_Decimal64",
#endif
};
static int is_keyword(const char *s, size_t n) {
for (size_t i = 0; i < countof(keywords); i++) {
if (strncmp(keywords[i], s, n) == 0) return 1;
}
return 0;
}
static int lex(enum_lex_t *lexer) {
lexer->p = skip(lexer->p, is_wspace);
lexer->s = NULL;
if (*lexer->p == '\0' || lexer->p >= lexer->e) return T_EOS;
if (is_alpha(*lexer->p) || *lexer->p == '_') {
lexer->s = lexer->p;
do { lexer->p++; } while (lexer->p < lexer->e && is_word(*lexer->p));
lexer->n = lexer->p - lexer->s;
if (strncmp(lexer->s, "enum", lexer->n) == 0) return T_KWENUM;
if (is_keyword(lexer->s, lexer->n)) return T_INVALIDKW;
return T_IDENTIFIER;
}
switch (*lexer->p) {
case '{': lexer->p++; return T_LBRACE;
case '}': lexer->p++; return T_RBRACE;
case ',': lexer->p++; return T_COMMA;
case '=': lexer->p++; return T_EQUALS;
default: return T_INVALID;
}
}
static unsigned init(enum_seq_t *seq, size_t size) {
seq->pool = calloc(size + 1, sizeof(*seq->pool));
if (!seq->pool) return (seq->errc = LIBENUM_E_ALLOC);
seq->name = "";
return LIBENUM_E_OK;
}
static char* skip(const char *str, int (*is_space)(char c)) {
assert(str);
for (;;) {
while (is_space(*str)) str++;
if (str[0] == '/' && str[1] == '/') {
str += 2;
while (str[0] && str[0] != '\r' && str[0] != '\n') str++;
continue;
}
if (str[0] == '/' && str[1] == '*') {
const char *start = str;
str += 2;
while (str[0] && str[1] && !(str[0] == '*' && str[1] == '/')) str++;
if (!str[0] || !str[1]) { str = start; break; } // unterminated
str += 2;
continue;
}
break;
}
return (char *)str;
}
enum_seq_t libenum_loadcpp(const char *str, size_t size, const char *prefix,
char **endptr) {
const char *p = str;
const char *e = str + size;
enum_seq_t result = { 0 };
if (init(&result, size) != LIBENUM_E_OK) return_error(result.errc);
char *p_pool = result.pool;
size_t cap = 256;
result.members = calloc(cap, sizeof(*result.members));
if (!result.members) return_error(LIBENUM_E_ALLOC);
size_t prefix_len = 0;
if (!prefix) prefix = "";
else prefix_len = strlen(prefix);
const char *s, *lf;
for (p = skip(p, is_hspace); p < e; p++) {
s = p;
lf = p;
while (lf < e && *lf != '\n') lf++; // TODO: escaped new-lines?
p = lf;
if (lf[-1] == '\r') lf--;
if (*s != '#') continue;
if (strncmp(s + 1, "define", lengthof("define")) != 0) continue;
// Parse out the symbol name
const char *sym_beg = skip(s + lengthof("#define"), is_hspace);
const char *sym_end = sym_beg;
if (sym_beg >= lf || (!is_alpha(*sym_beg) && *sym_beg != '_')) {
p = sym_beg;
return_error(LIBENUM_E_MISSING);
}
do { sym_end++; } while (sym_end < e && is_word(*sym_end));
if (is_vspace(*sym_end) || *sym_end == '(') continue;
if (!is_hspace(*sym_end)) {
p = sym_end;
return_error(LIBENUM_E_TOKEN);
}
if (strncmp(prefix, sym_beg, prefix_len) != 0) continue;
const char *expr_beg = skip(sym_end + 1, is_hspace);
const char *expr_end = lf;
if (result.size + 1 >= cap) {
const size_t new = cap * 2;
const size_t size = new * sizeof(*result.members);
enum_member_t *tmp = realloc(result.members, size);
if (!tmp) return_error(LIBENUM_E_ALLOC);
memset(&tmp[result.size], 0, cap * sizeof(*tmp));
result.members = tmp;
cap = new;
}
const size_t sym_len = sym_end - sym_beg;
const size_t expr_len = expr_end - expr_beg;
for (size_t i = 0; i < result.size; i++) {
if (strncmp(result.members[i].name, sym_beg, sym_len) == 0) {
p = sym_beg;
return_error(LIBENUM_E_DUPLICATE);
}
}
enum_member_t *memb = &result.members[result.size++];
p_pool = poolpush(&memb->name, p_pool, sym_beg, sym_len);
p_pool = poolpush(&memb->expr, p_pool, expr_beg, expr_len);
}
if (result.size == 0) free(result.members); // unlike enums, this is valid
if (endptr) *endptr = (char *)p;
return result;
error:
free(result.members);
free(result.pool);
if (endptr) *endptr = (char *)p;
result.members = NULL;
result.pool = NULL;
result.name = NULL;
result.size = 0;
return result;
}
const char* libenum_errs(unsigned errc) {
switch (errc) {
case LIBENUM_E_OK: return "(ok)";
case LIBENUM_E_ALLOC: return "allocation failure";
case LIBENUM_E_EOS: return "unexpected end-of-stream";
case LIBENUM_E_TOKEN: return "unexpected token in enum definition";
case LIBENUM_E_KEYWORD: return "stray keyword in enum definition";
case LIBENUM_E_STRAYMEMB: return "unexpected member definition";
case LIBENUM_E_DUPLICATE: return "duplicate member definition";
case LIBENUM_E_EMPTY: return "empty enum definition";
case LIBENUM_E_SEMICOLON: return "missing semicolon after enum definition";
case LIBENUM_E_MISSING: return "missing preprocessor substitution";
default: return "(unknown error code)";
}
}

120
tools/enumproc/libenum.h Normal file
View File

@@ -0,0 +1,120 @@
/*
* #include <libenum.h> - A library for loading C enums into member-mappings
* Copyright (C) 2026 <rachel@lhea.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LIBENUM_H
#define LIBENUM_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LIBENUM_E_OK 0
#define LIBENUM_E_ALLOC 1
#define LIBENUM_E_EOS 2
#define LIBENUM_E_TOKEN 3
#define LIBENUM_E_KEYWORD 4
#define LIBENUM_E_STRAYMEMB 5
#define LIBENUM_E_DUPLICATE 6
#define LIBENUM_E_EMPTY 7
#define LIBENUM_E_SEMICOLON 8
#define LIBENUM_E_MISSING 9
typedef struct enum_member enum_member_t;
typedef struct enum_seq enum_seq_t;
struct enum_member {
const char *name;
const char *expr;
};
struct enum_seq {
const char *name;
enum_member_t *members;
size_t size;
char *pool;
unsigned errc;
};
/**
* Load a C-style enum definition from `str` into a member-mapping.
*
* The resulting structure contains two allocations to be `free`d by the caller:
*
* 1. `members`, which contains the list of members.
* 2. `pool`, which is contains all zero-terminated strings present in the
* member-mapping.
*
* Each member's `name` field points to a zero-terminated string. If the value
* of a member is assigned by an integer-constant expression rather than as a
* natural successor, then the member's `expr` field points to a zero-terminated
* string for that expression. When natural succession is used, `expr` is set to
* `NULL`.
*
* If `endptr` is not `NULL`, then this routine will store the address of the
* semicolon which terminates the enum definition, if one is present. If the
* terminating semicolon is missing, then it is treated as an error.
*
* Members within the returned mapping are guaranteed to be uniquely named
* within the enum's scope. If parsing fails for any reason, then the returned
* mapping's `errc` value will be set to an associated error code. Additionally,
* if `endptr` is not `NULL`, the address of the first invalid character for the
* last-processed token will be stored in `*endptr`.
*/
enum_seq_t libenum_load(const char *str, size_t size, char **endptr);
/**
* Load all C preprocessor `#define` directives from `str`, as if they were
* defined with a C-style enum using assignment expressions.
*
* Functional macros and macros without substitution-text are skipped. Macros
* which escape new-lines in their substitution-text are not supported and are
* treated as if those escapes do not exist.
*
* If `prefix` is not `NULL`, then only substitutions which begin with `prefix`
* will be loaded into the resulting member-mapping.
*
* If `endptr` is not `NULL`, then this routine will store the address of the
* last-processed character on return.
*
* Members within the returned mapping are guaranteed to be uniquely named
* across all other mapped `#define` directives.
*/
enum_seq_t libenum_loadcpp(const char *str, size_t size, const char *prefix,
char **endptr);
/**
* Find a C-style enum definition from `str` with a given `name` and load it
* into a member-mapping, as in `libenum_load`. If no such instance is found,
* then the returned mapping will be all zeroes.
*/
enum_seq_t libenum_find(const char *str, size_t size, const char *name,
char **endptr);
/**
* Get a descriptive string for the provided error code. The returned string
* is statically-allocated and does not need to be `free`d.
*/
const char* libenum_errs(unsigned errc);
#ifdef __cplusplus
}
#endif
#endif // LIBENUM_H

361
tools/enumproc/libexpr.c Normal file
View File

@@ -0,0 +1,361 @@
/*
* #include <libexpr.h> - A library for evaluating integer-constant expressions
* Copyright (C) 2026 <rachel@lhea.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "libexpr.h"
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
static inline bool is_space(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static inline bool is_alpha(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
static inline bool is_digit(char c) {
return (c >= '0' && c <= '9');
}
static inline bool is_word(char c) {
return is_alpha(c) || is_digit(c) || c == '_';
}
static char* skip(const char *str) {
assert(str);
for (;;) {
while (is_space(*str)) str++;
if (str[0] == '/' && str[1] == '/') {
str += 2;
while (str[0] && str[0] != '\r' && str[0] != '\n') str++;
continue;
}
if (str[0] == '/' && str[1] == '*') {
const char *start = str;
str += 2;
while (str[0] && str[1] && !(str[0] == '*' && str[1] == '/')) str++;
if (!str[0] || !str[1]) { str = start; break; } // unterminated
str += 2;
continue;
}
break;
}
return (char *)str;
}
typedef struct evalstack evalstack_t;
struct evalstack {
long nums[16];
size_t n_nums;
int opts[16];
size_t n_opts;
int unas[16];
size_t n_unas;
};
#define precedes(o1, o2) opt_data[o2].prec < opt_data[o1].prec
#define congruent(o1, o2) opt_data[o2].prec == opt_data[o1].prec
#define is_left(opt) opt_data[opt].left
#define top(stk, var) stk.var[stk.n_##var - 1]
#define pop(stk, var) stk.var[--stk.n_##var]
#define push(stk, var, val) stk.var[stk.n_##var++] = val
#define evalbinary(o) \
do { \
assert(stk.n_nums >= 2); \
stk.n_nums -= 2; \
\
long *p_args = &stk.nums[stk.n_nums]; \
long result = 0; \
switch (o) { \
case OP_MUL: result = p_args[0] * p_args[1]; break; \
case OP_DIV: result = p_args[0] / p_args[1]; break; \
case OP_MOD: result = p_args[0] % p_args[1]; break; \
case OP_ADD: result = p_args[0] + p_args[1]; break; \
case OP_SUB: result = p_args[0] - p_args[1]; break; \
case OP_LSH: result = p_args[0] << p_args[1]; break; \
case OP_RSH: result = p_args[0] >> p_args[1]; break; \
case OP_LST: result = p_args[0] < p_args[1]; break; \
case OP_LTE: result = p_args[0] <= p_args[1]; break; \
case OP_GRT: result = p_args[0] > p_args[1]; break; \
case OP_GTE: result = p_args[0] >= p_args[1]; break; \
case OP_DEQ: result = p_args[0] == p_args[1]; break; \
case OP_NEQ: result = p_args[0] != p_args[1]; break; \
case OP_BAN: result = p_args[0] & p_args[1]; break; \
case OP_XOR: result = p_args[0] ^ p_args[1]; break; \
case OP_BOR: result = p_args[0] | p_args[1]; break; \
case OP_AND: result = p_args[0] && p_args[1]; break; \
case OP_ORR: result = p_args[0] || p_args[1]; break; \
\
default: assert(0 && "illegal op-type"); \
} \
\
push(stk, nums, result); \
} while (0)
#define evalunary(o) \
do { \
assert(stk.n_nums >= 1); \
\
long res = pop(stk, nums); \
switch (o) { \
case OP_POS: res = res < 0 ? -res : res; break; \
case OP_NEG: res = res > 0 ? -res : res; break; \
case OP_NOT: res = !res; break; \
case OP_BNT: res = ~res; break; \
\
default: assert(0 && "illegal op-type"); \
} \
\
push(stk, nums, res); \
} while (0)
// ref: https://en.wikipedia.org/wiki/Shunting_yard_algorithm
long libexpr_eval(const char *expr, char **endptr, scope_t *scope) {
enum op {
OP_LPA,
OP_RPA,
OP_POS,
OP_NEG,
OP_NOT,
OP_BNT,
OP_MUL,
OP_DIV,
OP_MOD,
OP_ADD,
OP_SUB,
OP_LSH,
OP_RSH,
OP_LST,
OP_LTE,
OP_GRT,
OP_GTE,
OP_DEQ,
OP_NEQ,
OP_BAN,
OP_XOR,
OP_BOR,
OP_AND,
OP_ORR,
};
// ref: https://en.cppreference.com/w/c/language/operator_precedence.html
static const struct { int prec; bool left; } opt_data[] = {
[OP_LPA] = { .prec = 1, .left = true },
[OP_RPA] = { .prec = 1, .left = true },
[OP_POS] = { .prec = 2, .left = false },
[OP_NEG] = { .prec = 2, .left = false },
[OP_NOT] = { .prec = 2, .left = false },
[OP_BNT] = { .prec = 2, .left = false },
[OP_MUL] = { .prec = 3, .left = true },
[OP_DIV] = { .prec = 3, .left = true },
[OP_MOD] = { .prec = 3, .left = true },
[OP_ADD] = { .prec = 4, .left = true },
[OP_SUB] = { .prec = 4, .left = true },
[OP_LSH] = { .prec = 5, .left = true },
[OP_RSH] = { .prec = 5, .left = true },
[OP_LST] = { .prec = 6, .left = true },
[OP_LTE] = { .prec = 6, .left = true },
[OP_GRT] = { .prec = 6, .left = true },
[OP_GTE] = { .prec = 6, .left = true },
[OP_DEQ] = { .prec = 7, .left = true },
[OP_NEQ] = { .prec = 7, .left = true },
[OP_BAN] = { .prec = 8, .left = true },
[OP_XOR] = { .prec = 9, .left = true },
[OP_BOR] = { .prec = 10, .left = true },
[OP_AND] = { .prec = 11, .left = true },
[OP_ORR] = { .prec = 12, .left = true },
};
int o1, o2;
const char *p = skip(expr);
char *e = NULL;
bool u = true, n = true;
long m = 1;
evalstack_t stk = { 0 };
while (*p) {
if (is_alpha(*p) || *p == '_') {
if (!n) goto early_exit;
const char *var_beg = p;
do { p++; } while (is_word(*p));
const char *var_end = p;
const size_t var_len = var_end - var_beg;
var_t *match = NULL;
for (size_t i = 0; scope && i < scope->len && !match; i++) {
if (strncmp(scope->vars[i].name, var_beg, var_len) == 0) {
match = &scope->vars[i];
}
}
if (!match) {
p = var_beg;
goto early_exit; // undefined symbol
}
push(stk, nums, match->value);
while (stk.n_unas > 0) {
o1 = pop(stk, unas);
evalunary(o1);
}
p = skip(p);
n = false;
u = false;
continue;
}
switch (*p) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
if (!n) goto early_exit;
push(stk, nums, strtol(p, &e, 0));
while (stk.n_unas > 0) {
o1 = pop(stk, unas);
evalunary(o1);
}
p = e;
n = false;
u = false;
break;
#define prepunary(opt) do { push(stk, unas, opt); p++; n = true; } while (0)
#define procbinary(opt, len) do { o1 = opt; p += len; goto handle_binary; } while (0)
case '+':
if (u) prepunary(OP_POS);
else procbinary(OP_ADD, 1);
break;
case '-':
if (u) prepunary(OP_NEG);
else procbinary(OP_SUB, 1);
break;
case '!':
if (u) prepunary(OP_NOT);
else {
assert(p[1] == '=' && "invalid operator (expected !=)");
procbinary(OP_NEQ, 2);
}
break;
case '=':
assert(p[1] == '=' && "invalid operator (expected ==)");
procbinary(OP_DEQ, 2);
break;
case '*': procbinary(OP_MUL, 1);
case '/': procbinary(OP_DIV, 1);
case '%': procbinary(OP_MOD, 1);
case '^': procbinary(OP_XOR, 1);
case '~': prepunary(OP_BNT); break;
case '<':
if (p[1] == '<') procbinary(OP_LSH, 2);
if (p[1] == '=') procbinary(OP_LTE, 2);
procbinary(OP_LST, 1);
case '>':
if (p[1] == '>') procbinary(OP_RSH, 2);
if (p[1] == '=') procbinary(OP_GTE, 2);
procbinary(OP_GRT, 1);
case '&':
if (p[1] == '&') procbinary(OP_AND, 2);
procbinary(OP_BAN, 1);
case '|':
if (p[1] == '|') procbinary(OP_ORR, 2);
procbinary(OP_BOR, 1);
handle_binary:
while (stk.n_opts > 0
&& (o2 = top(stk, opts)) != OP_LPA
&& (precedes(o1, o2) || (congruent(o1, o2) && is_left(o1)))) {
o2 = pop(stk, opts);
evalbinary(o2);
}
u = true;
n = true;
push(stk, opts, o1);
break;
#undef procbinary
#undef prepunary
case '(': push(stk, opts, OP_LPA); p++; u = true; n = true; break;
case ')':
o2 = -1;
while (stk.n_opts > 0 && (o2 = pop(stk, opts)) != OP_LPA) {
evalbinary(o2);
}
if (o2 != OP_LPA) goto early_exit;
long v = pop(stk, nums) * m;
push(stk, nums, v);
while (stk.n_unas > 0) {
o1 = pop(stk, unas);
evalunary(o1);
}
n = false;
u = false;
p++;
break;
default: goto early_exit;
}
p = skip(p);
}
early_exit:
while (stk.n_opts > 0) {
o2 = pop(stk, opts);
assert(o2 != OP_LPA && "unclosed parentheses");
evalbinary(o2);
}
while (stk.n_unas > 0) {
o1 = pop(stk, unas);
evalunary(o1);
}
if (endptr) *endptr = (char *)p;
return stk.nums[0];
}

56
tools/enumproc/libexpr.h Normal file
View File

@@ -0,0 +1,56 @@
/*
* #include <libexpr.h> - A library for evaluating integer-constant expressions
* Copyright (C) 2026 <rachel@lhea.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LIBEXPR_H
#define LIBEXPR_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
typedef struct var var_t;
struct var {
long value;
const char *name;
};
typedef struct scope scope_t;
struct scope {
var_t *vars;
size_t len;
};
/**
* Evaluate a integer-constant expression from a zero-terminated string `expr`.
*
* Variable-identifiers will be replaced with their corresponding value in
* `scope`, if found. Otherwise, the result is an error.
*
* On return, if `endptr` is not `NULL`, the address of the last-processed
* character will be stored in `*endptr`. That is, the expression is valid if
* `*endptr` points to the zero-terminator for `expr`.
*/
long libexpr_eval(const char *expr, char **endptr, scope_t *scope);
#ifdef __cplusplus
}
#endif
#endif // LIBEXPR_H

View File

@@ -0,0 +1,39 @@
libenum_dep = declare_dependency(
include_directories: include_directories('.'),
link_with: static_library(
'libenum',
sources: files('libenum.c'),
c_args: [
'-std=gnu17',
'-O3',
'-Wall',
'-Wextra',
'-Wpedantic',
'-Wconversion',
'-Wno-sign-conversion',
'-Werror',
],
include_directories: include_directories('.'),
native: true,
),
)
libexpr_dep = declare_dependency(
include_directories: include_directories('.'),
link_with: static_library(
'libexpr',
sources: files('libexpr.c'),
c_args: [
'-std=gnu17',
'-O3',
'-Wall',
'-Wextra',
'-Wpedantic',
'-Wconversion',
'-Wno-sign-conversion',
'-Werror',
],
include_directories: include_directories('.'),
native: true,
),
)

View File

@@ -1,8 +1,11 @@
rapidjson_dep = dependency('rapidjson')
# Native tools
subdir('nitroarc') # Contains a library component that other tools depend on
subdir('dataproc')
# Native tools with helper libraries
subdir('nitroarc') # libnitroarc
subdir('enumproc') # libenum
subdir('dataproc') # Requires libnitroarc and libenum
# Other native tools
subdir('csv2bin')
subdir('datagen')
subdir('fixrom')