From 54e7939d73c5f6f8791be989783df0a1bf4e2e8b Mon Sep 17 00:00:00 2001 From: Rachel Date: Mon, 20 Apr 2026 22:25:35 -0700 Subject: [PATCH] tools: Extract enum parsing to more generic libenum --- tools/dataproc/meson.build | 9 +- tools/dataproc/src/common.c | 90 ++++- tools/dataproc/src/common.h | 2 +- tools/dataproc/src/enum.c | 273 ------------- tools/dataproc/src/enum.h | 78 ---- tools/devtools/gen_compile_commands.py | 22 ++ tools/enumproc/libenum.c | 509 +++++++++++++++++++++++++ tools/enumproc/libenum.h | 120 ++++++ tools/enumproc/libexpr.c | 361 ++++++++++++++++++ tools/enumproc/libexpr.h | 56 +++ tools/enumproc/meson.build | 39 ++ tools/meson.build | 9 +- 12 files changed, 1201 insertions(+), 367 deletions(-) delete mode 100644 tools/dataproc/src/enum.c delete mode 100644 tools/dataproc/src/enum.h create mode 100644 tools/enumproc/libenum.c create mode 100644 tools/enumproc/libenum.h create mode 100644 tools/enumproc/libexpr.c create mode 100644 tools/enumproc/libexpr.h create mode 100644 tools/enumproc/meson.build diff --git a/tools/dataproc/meson.build b/tools/dataproc/meson.build index e7d572c4a4..040d74b65e 100644 --- a/tools/dataproc/meson.build +++ b/tools/dataproc/meson.build @@ -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 ], diff --git a/tools/dataproc/src/common.c b/tools/dataproc/src/common.c index 8985dcfdac..4e952dd879 100644 --- a/tools/dataproc/src/common.c +++ b/tools/dataproc/src/common.c @@ -10,11 +10,19 @@ #include #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; } diff --git a/tools/dataproc/src/common.h b/tools/dataproc/src/common.h index 2a3d56e305..205b81a2d4 100644 --- a/tools/dataproc/src/common.h +++ b/tools/dataproc/src/common.h @@ -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); diff --git a/tools/dataproc/src/enum.c b/tools/dataproc/src/enum.c deleted file mode 100644 index 1e4d534ad0..0000000000 --- a/tools/dataproc/src/enum.c +++ /dev/null @@ -1,273 +0,0 @@ -#include "enum.h" - -#include -#include -#include -#include -#include - -// 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; -} diff --git a/tools/dataproc/src/enum.h b/tools/dataproc/src/enum.h deleted file mode 100644 index d9fec70887..0000000000 --- a/tools/dataproc/src/enum.h +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef ENUM_H -#define ENUM_H - -#include -#include - -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 diff --git a/tools/devtools/gen_compile_commands.py b/tools/devtools/gen_compile_commands.py index 0ae2fdebcb..2738fb96b6 100755 --- a/tools/devtools/gen_compile_commands.py +++ b/tools/devtools/gen_compile_commands.py @@ -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}", diff --git a/tools/enumproc/libenum.c b/tools/enumproc/libenum.c new file mode 100644 index 0000000000..0ed79a9af3 --- /dev/null +++ b/tools/enumproc/libenum.c @@ -0,0 +1,509 @@ +/* + * #include - A library for loading C enums into member-mappings + * Copyright (C) 2026 + * + * 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 . + */ + +#include "libenum.h" + +#include +#include +#include +#include + +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)"; + } +} diff --git a/tools/enumproc/libenum.h b/tools/enumproc/libenum.h new file mode 100644 index 0000000000..95d7c9bc65 --- /dev/null +++ b/tools/enumproc/libenum.h @@ -0,0 +1,120 @@ +/* + * #include - A library for loading C enums into member-mappings + * Copyright (C) 2026 + * + * 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 . + */ + +#ifndef LIBENUM_H +#define LIBENUM_H + +#include + +#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 diff --git a/tools/enumproc/libexpr.c b/tools/enumproc/libexpr.c new file mode 100644 index 0000000000..c7ac3ad0e3 --- /dev/null +++ b/tools/enumproc/libexpr.c @@ -0,0 +1,361 @@ +/* + * #include - A library for evaluating integer-constant expressions + * Copyright (C) 2026 + * + * 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 . + */ + +#include "libexpr.h" + +#include +#include +#include +#include +#include + +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]; +} diff --git a/tools/enumproc/libexpr.h b/tools/enumproc/libexpr.h new file mode 100644 index 0000000000..e580c42545 --- /dev/null +++ b/tools/enumproc/libexpr.h @@ -0,0 +1,56 @@ +/* + * #include - A library for evaluating integer-constant expressions + * Copyright (C) 2026 + * + * 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 . + */ + +#ifndef LIBEXPR_H +#define LIBEXPR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +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 diff --git a/tools/enumproc/meson.build b/tools/enumproc/meson.build new file mode 100644 index 0000000000..0818eba5b6 --- /dev/null +++ b/tools/enumproc/meson.build @@ -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, + ), +) diff --git a/tools/meson.build b/tools/meson.build index 3f55b68204..d208754d53 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -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')