From 5e921bf59373d79d27c322ff86e8b5a37b151e45 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sun, 31 Jul 2022 16:02:10 +0100 Subject: Add magical feature codegen system, at long last --- src/build/cmeta.c | 83 ++++++++++++- src/build/cmeta.h | 33 ++++++ src/build/codegen.c | 326 ++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 401 insertions(+), 41 deletions(-) (limited to 'src/build') diff --git a/src/build/cmeta.c b/src/build/cmeta.c index 3d49927..260d33f 100644 --- a/src/build/cmeta.c +++ b/src/build/cmeta.c @@ -20,6 +20,7 @@ #include "../intdefs.h" #include "../os.h" +#include "cmeta.h" /* * This file does C metadata parsing/scraping for the build system. This @@ -31,10 +32,12 @@ * time. Don't worry about it too much. */ -// too lazy to write a C tokenizer at the moment, so let's just yoink some code -// from a hacked-up copy of chibicc, a nice minimal C compiler with code that's -// pretty easy to work with. it does leak memory by design, but build stuff is -// all one-shot so that's fine. +// lazy inlined 3rd party stuff {{{ +// too lazy to write a C tokenizer at the moment, or indeed probably ever, so +// let's just yoink some code from a hacked-up copy of chibicc, a nice minimal C +// compiler with code that's pretty easy to work with. it does leak memory by +// design, but build stuff is all one-shot so that's fine. +#include "../3p/chibicc/chibicc.h" #include "../3p/chibicc/unicode.c" // type sentinels from type.c (don't bring in the rest of type.c because it // circularly depends on other stuff and we really only want tokenize here) @@ -88,6 +91,7 @@ static char *join_tokens(Token *tok, Token *end) { buf[pos] = '\0'; return buf; } +// }}} #ifdef _WIN32 #include "../3p/openbsd/asprintf.c" // missing from libc; plonked here for now @@ -266,7 +270,74 @@ void cmeta_conmacros(const struct cmeta *cm, } } -void cmeta_evdefmacros(const struct cmeta *cm, void (*cb_def)(const char *name)) { +const char *cmeta_findfeatmacro(const struct cmeta *cm) { + Token *tp = (Token *)cm; + if (!tp || !tp->next) return 0; // FEATURE, ( + while (tp) { + if (equal(tp, "FEATURE") && equal(tp->next, "(")) { + if (equal(tp->next->next, ")")) return ""; // no arg = no desc + if (!tp->next->next || tp->next->next->kind != TK_STR) { + return 0; // it's invalid, whatever, just return... + } + return tp->next->next->str; + } + tp = tp->next; + } + return 0; +} + +void cmeta_featinfomacros(const struct cmeta *cm, void (*cb)( + enum cmeta_featmacro type, const char *param, void *ctxt), void *ctxt) { + Token *tp = (Token *)cm; + if (!tp || !tp->next) return; + while (tp) { + int type = -1; + if (equal(tp, "PREINIT")) { + type = CMETA_FEAT_PREINIT; + } + else if (equal(tp, "INIT")) { + type = CMETA_FEAT_INIT; + } + else if (equal(tp, "END")) { + type = CMETA_FEAT_END; + } + if (type != - 1) { + if (equal(tp->next, "{")) { + cb(type, 0, ctxt); + tp = tp->next; + } + tp = tp->next; + continue; + } + if (equal(tp, "REQUIRE")) { + type = CMETA_FEAT_REQUIRE; + } + else if (equal(tp, "REQUIRE_GAMEDATA")) { + type = CMETA_FEAT_REQUIREGD; + } + else if (equal(tp, "REQUIRE_GLOBAL")) { + type = CMETA_FEAT_REQUIREGLOBAL; + } + else if (equal(tp, "REQUEST")) { + type = CMETA_FEAT_REQUEST; + } + if (type != -1) { + if (equal(tp->next, "(") && tp->next->next) { + tp = tp->next->next; + char *param = malloc(tp->len + 1); + if (!param) die1("couldn't allocate memory"); + memcpy(param, tp->loc, tp->len); + param[tp->len] = '\0'; + cb(type, param, ctxt); + tp = tp->next; + } + } + tp = tp->next; + } +} + +void cmeta_evdefmacros(const struct cmeta *cm, + void (*cb_def)(const char *name)) { Token *tp = (Token *)cm; if (!tp || !tp->next || !tp->next->next) return; // DEF_EVENT, (, name while (tp) { @@ -298,4 +369,4 @@ void cmeta_evhandlermacros(const struct cmeta *cm, const char *modname, } } -// vi: sw=4 ts=4 noet tw=80 cc=80 +// vi: sw=4 ts=4 noet tw=80 cc=80 fdm=marker diff --git a/src/build/cmeta.h b/src/build/cmeta.h index 125ce2c..40c4ac5 100644 --- a/src/build/cmeta.h +++ b/src/build/cmeta.h @@ -39,6 +39,39 @@ void cmeta_includes(const struct cmeta *cm, void cmeta_conmacros(const struct cmeta *cm, void (*cb)(const char *name, bool isvar, bool unreg)); +/* + * Looks for a feature description macro in file, returning the description + * string if it exists, an empty string if the feature is defined without a + * user-facing description, and null if source file does not define a feature. + */ +const char *cmeta_findfeatmacro(const struct cmeta *cm); + +/* + * the various kinds of feature specficiation macros, besides the feature + * declaration macro itself + */ +enum cmeta_featmacro { + CMETA_FEAT_REQUIRE, + CMETA_FEAT_REQUIREGD, + CMETA_FEAT_REQUIREGLOBAL, + CMETA_FEAT_REQUEST, + CMETA_FEAT_PREINIT, + CMETA_FEAT_INIT, + CMETA_FEAT_END +}; + +/* + * Iterates through all feature dependency macros and init/end/preinit + * indicators, passing each bit of information to the callback cb. + * + * PREINT, INIT and END macros don't pass anything to param. + * + * This one takes a context pointer, while the others don't, because this is all + * cobbled together without much consistent abstraction. + */ +void cmeta_featinfomacros(const struct cmeta *cm, void (*cb)( + enum cmeta_featmacro type, const char *param, void *ctxt), void *ctxt); + /* * Iterates through all event-related macros and takes note of which events are * defined, giving a callback for each. diff --git a/src/build/codegen.c b/src/build/codegen.c index 1dee6a1..04a9058 100644 --- a/src/build/codegen.c +++ b/src/build/codegen.c @@ -14,35 +14,37 @@ * PERFORMANCE OF THIS SOFTWARE. */ +#include #include #include #include +#include "../intdefs.h" #include "../os.h" #include "cmeta.h" #include "skiplist.h" #include "vec.h" -#define MAXENT 65536 // arbitrary limit! -static struct ent { - const char *name; - bool unreg; - bool isvar; // false for cmd -} ents[MAXENT]; -static int nents; - static void die(const char *s) { fprintf(stderr, "codegen: %s\n", s); exit(100); } +#define MAXENT 65536 // arbitrary limit! +static struct conent { + const char *name; + bool unreg; + bool isvar; // false for cmd +} conents[MAXENT]; +static int nconents; + #define PUT(name_, isvar_, unreg_) do { \ - if (nents == sizeof(ents) / sizeof(*ents)) { \ + if (nconents == sizeof(conents) / sizeof(*conents)) { \ fprintf(stderr, "codegen: out of space; make ents bigger!\n"); \ exit(1); \ } \ - ents[nents].name = name_; \ - ents[nents].isvar = isvar_; ents[nents++].unreg = unreg_; \ + conents[nconents].name = name_; \ + conents[nconents].isvar = isvar_; conents[nconents++].unreg = unreg_; \ } while (0) static void oncondef(const char *name, bool isvar, bool unreg) { @@ -50,10 +52,95 @@ static void oncondef(const char *name, bool isvar, bool unreg) { } struct vec_str VEC(const char *); +struct vec_usize VEC(usize); +struct vec_featp VEC(struct feature *); + +enum { UNSEEN, SEEING, SEEN }; + +DECL_SKIPLIST(static, feature, struct feature, const char *, 4) +DECL_SKIPLIST(static, feature_bydesc, struct feature, const char *, 4) +struct feature { + const char *modname; + const char *desc; + const struct cmeta *cm; // backref for subsequent options pass + struct vec_featp needs; + // keep optionals in a separate array mainly so we have separate counts + struct vec_featp wants; + uint dfsstate : 2; // used for sorting and cycle checking + bool has_preinit : 1, /*has_init : 1, <- required anyway! */ has_end : 1; + bool has_evhandlers : 1; + bool is_requested : 1; // determines if has_ variable needs to be extern + //char pad : 2; + //char pad[3]; + struct vec_str need_gamedata; + struct vec_str need_globals; + struct skiplist_hdr_feature hdr; // by id/modname + struct skiplist_hdr_feature_bydesc hdr_bydesc; +}; +static inline int cmp_feature(struct feature *e, const char *s) { + return strcmp(e->modname, s); +} +static inline int cmp_feature_bydesc(struct feature *e, const char *s) { + for (const char *p = e->desc; ; ++p, ++s) { + // longer string first + if (!*s) return !!*p; else if (!*p) return -1; + // case insensitive sort where possible + if (tolower(*p) > tolower(*s)) return 1; + if (tolower(*p) < tolower(*s)) return -1; + // prioritise upper-case if same letter + if (isupper(*p) && islower(*s)) return 1; + if (islower(*p) && isupper(*s)) return -1; + } + return 0; +} +static inline struct skiplist_hdr_feature *hdr_feature(struct feature *e) { + return &e->hdr; +} +static inline struct skiplist_hdr_feature_bydesc *hdr_feature_bydesc( + struct feature *e) { + return &e->hdr_bydesc; +} +DEF_SKIPLIST(static, feature, cmp_feature, hdr_feature) +DEF_SKIPLIST(static, feature_bydesc, cmp_feature_bydesc, hdr_feature_bydesc) +static struct skiplist_hdr_feature features = {0}; +// sort in two different ways, so we can alphabetise the user-facing display +// NOTE: not all features will show up in this second list! +static struct skiplist_hdr_feature_bydesc features_bydesc = {0}; + +static void onfeatinfo(enum cmeta_featmacro type, const char *param, + void *ctxt) { + struct feature *f = ctxt; + switch (type) { + case CMETA_FEAT_REQUIRE:; bool optional = false; goto dep; + case CMETA_FEAT_REQUEST: optional = true; +dep: struct feature *dep = skiplist_get_feature(&features, param); + if (optional) dep->is_requested = true; + if (!dep) { + fprintf(stderr, "codegen: error: feature `%s` tried to depend " + "on non-existent feature `%s`\n", f->modname, param); + exit(1); \ + } + if (!vec_push(optional ? &f->wants : &f->needs, dep)) { + die("couldn't allocate memory"); + } + break; + case CMETA_FEAT_REQUIREGD:; + struct vec_str *vecp = &f->need_gamedata; + goto push; + case CMETA_FEAT_REQUIREGLOBAL: + vecp = &f->need_globals; +push: if (!vec_push(vecp, param)) die("couldn't allocate memory"); + break; + case CMETA_FEAT_PREINIT: f->has_preinit = true; break; + case CMETA_FEAT_END: f->has_end = true; break; + case CMETA_FEAT_INIT:; // nop for now, I guess + } +} + DECL_SKIPLIST(static, event, struct event, const char *, 4) -struct event { + struct event { const char *name; - struct vec_str handlers; + struct vec_usize handlers; // strings, but with tagged pointers - see below struct skiplist_hdr_event hdr; }; static inline int cmp_event(struct event *e, const char *s) { @@ -63,7 +150,7 @@ static inline struct skiplist_hdr_event *hdr_event(struct event *e) { return &e->hdr; } DEF_SKIPLIST(static, event, cmp_event, hdr_event) -struct skiplist_hdr_event events = {0}; +static struct skiplist_hdr_event events = {0}; static void onevdef(const char *name) { struct event *e = skiplist_get_event(&events, name); @@ -71,7 +158,7 @@ static void onevdef(const char *name) { struct event *e = malloc(sizeof(*e)); if (!e) die("couldn't allocate memory"); e->name = name; - e->handlers = (struct vec_str){0}; + e->handlers = (struct vec_usize){0}; e->hdr = (struct skiplist_hdr_event){0}; skiplist_insert_event(&events, name, e); } @@ -89,24 +176,95 @@ static void onevhandler(const char *evname, const char *modname) { "non-existent event `%s`\n", modname, evname); exit(2); } + usize taggedptr = (usize)modname; + struct feature *f = skiplist_get_feature(&features, modname); + // hack: using unused pointer bit to determine whether a handler is tied to + // a feature and thus conditional. relies on malloc alignment! + if (f) taggedptr |= 1ull; // NOTE: not bothering to check for more than one handler in a file. // compiler will get that anyway. - if (!vec_push(&e->handlers, modname)) die("couldn't allocate memory"); + if (!vec_push(&e->handlers, taggedptr)) die("couldn't allocate memory"); } +struct passinfo { + const struct cmeta *cm; + const os_char *path; +}; +static struct vec_passinfo VEC(struct passinfo) pass2 = {0}; + #define _(x) \ if (fprintf(out, "%s\n", x) < 0) die("couldn't write to file"); #define F(f, ...) \ if (fprintf(out, f "\n", __VA_ARGS__) < 0) die("couldn't write to file"); -#define H() \ -_( "/* This file is autogenerated by src/build/codegen.c. DO NOT EDIT! */") \ -_( "") +#define H_() \ + _( "/* This file is autogenerated by "__FILE__". DO NOT EDIT! */") +#define H() H_() _( "") -struct passinfo { - const struct cmeta *cm; - const os_char *path; -}; -struct vec_passinfo VEC(struct passinfo) pass2 = {0}; +static struct vec_featp endstack = {0}; // stack for reversing order + +static void featdfs(FILE *out, struct feature *f) { + if (f->dfsstate == SEEN) return; + if (f->dfsstate == SEEING) { + // XXX: could unwind for full cycle listing like in build. + // purely being lazy by not doing that here, and assuming there won't + // actually be cycles anyway, because this is not a general purpose tool + // and people working on this codebase are very smart. + fprintf(stderr, "codegen: error: dependency cycle found at feature `%s`\n", + f->modname); + exit(2); + } + f->dfsstate = SEEING; + // easier to do wants first, then we can do the conditional counter nonsense + // without worrying about how that fits in... + for (struct feature *const *pp = f->wants.data; + pp - f->wants.data < f->wants.sz; ++pp) { + featdfs(out, *pp); + } +F( " char status_%s = FEAT_OK;", f->modname); + const char *else_ = ""; + if (f->needs.sz == 1) { + featdfs(out, f->needs.data[0]); +F( " if (status_%s != FEAT_OK) status_%s = FEAT_REQFAIL;", + f->needs.data[0]->modname, f->modname) + else_ = "else "; + } + else if (f->needs.sz > 1) { + for (struct feature *const *pp = f->needs.data; + pp - f->needs.data < f->needs.sz; ++pp) { + featdfs(out, *pp); + } +F( " bool metdeps_%s =", f->modname) + for (struct feature *const *pp = f->needs.data; + pp - f->needs.data < f->needs.sz; ++pp) { +F( " status_%s == FEAT_OK%s", (*pp)->modname, + pp - f->needs.data == f->needs.sz - 1 ? ";" : " &&") // dumb but oh well + } +F( " if (!metdeps_%s) status_%s = FEAT_REQFAIL;", f->modname, f->modname) + else_ = "else "; + } + if (f->has_preinit) { +F( " %sif (!_feature_preinit_%s()) status_%s = FEAT_PREFAIL;", else_, + f->modname, f->modname); + else_ = "else "; + } + for (const char **pp = f->need_gamedata.data; + pp - f->need_gamedata.data < f->need_gamedata.sz; ++pp) { +F( " %sif (!has_%s) status_%s = FEAT_NOGD;", else_, *pp, f->modname) + else_ = "else "; // blegh + } + for (const char **pp = f->need_globals.data; + pp - f->need_globals.data < f->need_globals.sz; ++pp) { +F( " %sif (!%s) status_%s = FEAT_NOGLOBAL;", else_, *pp, f->modname) + else_ = "else "; // blegh 2 + } +F( " %sif (!_feature_init_%s()) status_%s = FEAT_FAIL;", else_, f->modname, + f->modname) + if (f->has_end || f->has_evhandlers || f->is_requested) { +F( " has_%s = status_%s == FEAT_OK;", f->modname, f->modname) + } + if (!vec_push(&endstack, f)) die("couldn't allocate memory"); + f->dfsstate = SEEN; +} int OS_MAIN(int argc, os_char *argv[]) { for (++argv; *argv; ++argv) { @@ -118,8 +276,8 @@ int OS_MAIN(int argc, os_char *argv[]) { } } - // we have to a second pass for event handlers. also, there's a bunch of - // terrible garbage here. don't stare for too long... + // we have to do a second pass for features and event handlers. also, + // there's a bunch of terrible garbage here. don't stare for too long... for (struct passinfo *pi = pass2.data; pi - pass2.data < pass2.sz; ++pi) { // XXX: I guess we should cache these by name or something! const struct cmeta *cm = pi->cm; @@ -162,18 +320,36 @@ int OS_MAIN(int argc, os_char *argv[]) { else if (!strcmp(modname, "sst")) { continue; // I guess??? } + const char *featdesc = cmeta_findfeatmacro(cm); + if (featdesc) { + struct feature *f = malloc(sizeof(*f)); + if (!f) die("couldn't allocate memory"); + *f = (struct feature){ + .modname = modname, + .desc = featdesc[0] ? featdesc : 0, + .cm = cm + }; + skiplist_insert_feature(&features, modname, f); + if (f->desc) { + skiplist_insert_feature_bydesc(&features_bydesc, f->desc, f); + } + } cmeta_evhandlermacros(cm, modname, &onevhandler); } + // yet another pass because I am stupid and don't want to think harder :) + for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) { + cmeta_featinfomacros(f->cm, &onfeatinfo, f); + } FILE *out = fopen(".build/include/cmdinit.gen.h", "wb"); if (!out) die("couldn't open cmdinit.gen.h"); - H(); - for (const struct ent *p = ents; p - ents < nents; ++p) { +H(); + for (const struct conent *p = conents; p - conents < nconents; ++p) { F( "extern struct con_%s *%s;", p->isvar ? "var" : "cmd", p->name) } _( "") _( "static void regcmds(void) {") - for (const struct ent *p = ents; p - ents < nents; ++p) { + for (const struct conent *p = conents; p - conents < nconents; ++p) { if (p->isvar) { F( " initval(%s);", p->name) } @@ -184,23 +360,103 @@ F( " con_reg(%s);", p->name) _( "}") _( "") _( "static void freevars(void) {") - for (const struct ent *p = ents; p - ents < nents; ++p) { + for (const struct conent *p = conents; p - conents < nconents; ++p) { if (p->isvar) { -F( " extfree(%s->strval);", p->name); +F( " extfree(%s->strval);", p->name) } } _( "}") if (fclose(out) == EOF) die("couldn't fully write cmdinit.gen.h"); + out = fopen(".build/include/featureinit.gen.h", "wb"); + if (!out) die("couldn't open featureinit.gen.h"); + H() + // XXX: I dunno whether this should just be defined in sst.c. It's sort of + // internal to the generated stuff hence tucking it away here, but that's at + // the cost of extra string-spaghettiness +_( "enum {") +_( " FEAT_OK,") +_( " FEAT_REQFAIL,") +_( " FEAT_PREFAIL,") +_( " FEAT_NOGD,") +_( " FEAT_NOGLOBAL,") +_( " FEAT_FAIL") +_( "};") +_( "") +_( "static const char *const featmsgs[] = {") +_( " \" [ OK! ] %s\\n\",") +_( " \" [ skipped ] %s (requires another feature)\\n\",") +_( " \" [ skipped ] %s (not applicable or useful)\\n\",") +_( " \" [ unsupported ] %s (missing gamedata)\\n\",") +_( " \" [ FAILED! ] %s (failed to access engine)\\n\",") +_( " \" [ FAILED! ] %s (error in initialisation)\\n\"") +_( "};") +_( "") + for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) { + if (f->has_preinit) { +F( "extern bool _feature_preinit_%s(void);", f->modname) + } +F( "extern bool _feature_init_%s(void);", f->modname) + if (f->has_end) { +F( "extern bool _feature_end_%s(void);", f->modname) + } + if (f->is_requested) { +F( "bool has_%s = false;", f->modname) + } + else if (f->has_end || f->has_evhandlers) { +F( "static bool has_%s = false;", f->modname) + } + } +_( "") +_( "static void initfeatures(void) {") + for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) { + featdfs(out, f); + } +_( "") + // note: old success message is moved in here, to get the ordering right +_( " con_colourmsg(RGBA(64, 255, 64, 255),") +_( " LONGNAME \" v\" VERSION \" successfully loaded\");") +_( " con_colourmsg(RGBA(255, 255, 255, 255), \" for game \");") +_( " con_colourmsg(RGBA(0, 255, 255, 255), \"%s\\n\", gameinfo_title);") +_( " struct con_colour white = {255, 255, 255, 255};") +_( " struct con_colour green = {128, 255, 128, 255};") +_( " struct con_colour red = {255, 128, 128, 255};") +_( " con_colourmsg(&white, \"---- List of plugin features ---\\n\");"); + for (const struct feature *f = features_bydesc.x[0]; f; + f = f->hdr_bydesc.x[0]) { +F( " con_colourmsg(status_%s == FEAT_OK ? &green : &red,", f->modname) +F( " featmsgs[(int)status_%s], \"%s\");", f->modname, f->desc) + } +_( "}") +_( "") +_( "static void endfeatures(void) {") + for (struct feature **pp = endstack.data + endstack.sz - 1; + pp - endstack.data >= 0; --pp) { + if ((*pp)->has_end) { +F( " if (has_%s) _feature_end_%s();", (*pp)->modname, (*pp)->modname) + } + } +_( "}") +_( "") + if (fclose(out) == EOF) die("couldn't fully write featureinit.gen.h"); + out = fopen(".build/include/evglue.gen.h", "wb"); if (!out) die("couldn't open evglue.gen.h"); - H() + H_() for (const struct event *e = events.x[0]; e; e = e->hdr.x[0]) { +_( "") F( "void _evemit_%s(void) {", e->name) - for (const char **pp = e->handlers.data; + for (usize *pp = e->handlers.data; pp - e->handlers.data < e->handlers.sz; ++pp) { -F( " void _evhandler_%s_%s(void);", *pp, e->name); // blegh. -F( " _evhandler_%s_%s();", *pp, e->name); + const char *modname = (const char *)(*pp & ~1ull); +F( " void _evhandler_%s_%s(void);", modname, e->name) // blegh. + if (*pp & 1ull) { + // note: has_* variables are already included by this point (above) +F( " if (has_%s) _evhandler_%s_%s();", modname, modname, e->name) + } + else { +F( " _evhandler_%s_%s();", modname, e->name) + } } _( "}") } -- cgit v1.2.3