diff options
Diffstat (limited to 'src/build/gluegen.c')
-rw-r--r-- | src/build/gluegen.c | 963 |
1 files changed, 963 insertions, 0 deletions
diff --git a/src/build/gluegen.c b/src/build/gluegen.c new file mode 100644 index 0000000..4a0f4a1 --- /dev/null +++ b/src/build/gluegen.c @@ -0,0 +1,963 @@ +/* + * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "../intdefs.h" +#include "../langext.h" +#include "../os.h" +#include "cmeta.h" + +#ifdef _WIN32 +#define fS "S" +#else +#define fS "s" +#endif + +static inline noreturn die(int status, const char *s) { + fprintf(stderr, "gluegen: fatal: %s\n", s); + exit(status); +} + +static inline noreturn diefile(int status, const os_char *f, int line, + const char *s) { + if (line) { + fprintf(stderr, "gluegen: fatal: %" fS ":%d: %s\n", f, line, s); + } + else { + fprintf(stderr, "gluegen: fatal: %" fS ": %s\n", f, s); + } + exit(status); +} + +#define ARENASZ (1024 * 1024) +static _Alignas(64) char _arena[ARENASZ] = {0}, *const arena = _arena - 64; +static int arena_last = 0; +static int arena_used = 64; // using 0 indices as null; reserve and stay aligned + +static inline void _arena_align(void) { + enum { ALIGN = ssizeof(void *) }; + if (arena_used & ALIGN - 1) arena_used = (arena_used + ALIGN) & ~(ALIGN - 1); +} + +static inline int arena_bump(int amt) { + if_cold (arena_used + amt >= ARENASZ) die(2, "out of arena memory"); + int ret = arena_used; + arena_used += amt; + return ret; +} + +static int arena_new(int sz) { + _arena_align(); + arena_last = arena_used; + int off = arena_bump(sz); + return off; +} + +#define LIST_MINALLOC 64 +#define LIST_MINSPACE (LIST_MINALLOC - ssizeof(struct list_chunkhdr)) + +struct list_chunkhdr { int next, sz; }; +struct list_chunk { struct list_chunkhdr hdr; char space[LIST_MINSPACE]; }; + +static struct list_grow_ret { + struct list_chunkhdr *tail; + void *data; +} list_grow(struct list_chunkhdr *tail, int amt) { + if_hot ((char *)tail == arena + arena_last) { + arena_bump(amt); + } + else if (tail->sz + amt > LIST_MINSPACE) { + int allocsz = ssizeof(struct list_chunkhdr) + amt; + int new = arena_new(allocsz > LIST_MINALLOC ? allocsz : LIST_MINALLOC); + struct list_chunkhdr *newptr = (struct list_chunkhdr *)(arena + new); + newptr->next = 0; newptr->sz = 0; + tail->next = new; + return (struct list_grow_ret){newptr, (char *)(newptr + 1)}; + } + struct list_grow_ret r = {tail, (char *)(tail + 1) + tail->sz}; + tail->sz += amt; + return r; +} + +static inline void *list_grow_p(struct list_chunkhdr **tailp, int amt) { + struct list_grow_ret r = list_grow(*tailp, amt); + *tailp = r.tail; + return r.data; +} + +#define list_append_p(T, tailp) ((T *)list_grow_p((tailp), ssizeof(T))) +#define list_append(tailp, x) ((void)(*list_append_p(typeof(x), (tailp)) = (x))) + +#define _list_foreach(T, varname, firstchunkp, extra) \ + for (struct list_chunkhdr *_h = &(firstchunkp)->hdr; _h; \ + _h = _h->next ? (struct list_chunkhdr *)(arena + _h->next) : 0) \ + for (typeof(T) *varname = (typeof(T) *)(_h + 1); \ + (char *)varname < (char *)(_h + 1) + _h->sz && (extra); \ + ++varname) \ + /* ... */ + +#define list_foreach_p(T, varname, firstchunkp) \ + _list_foreach(T, varname, firstchunkp, 1) + /* {...} */ + +#define list_foreach(T, varname, firstchunkp) \ + /* N.B. this crazy looking construct avoids unused warnings on varname + in case we just want to count up the entries or something */ \ + switch (0) for (typeof(T) varname; 0;) if ((void)varname, 0) case 0: \ + _list_foreach(T, _foreachp, firstchunkp, (varname = *_foreachp, 1)) \ + /* ... */ + +#define DEF_NEW(type, func, nvar, maxvar, desc) \ + static inline type func(void) { \ + if_cold (nvar == maxvar) { \ + die(2, "out of " desc " - increase " #maxvar " in gluegen.c!"); \ + } \ + return nvar++; \ + } + +// trickery to enable 1-based indexing (and thus 0-as-null) for various arrays +#define SHUNT(T, x) typeof(T) _array_##x[], *const x = _array_##x - 1, _array_##x + +#define MAX_MODULES 512 +// note that by 1-indexing these, we enable the radix stuff above to use the +// default initialisation as null and we *also* allow argc to be used as a +// direct index into each array when looping through files passed to main()! +// XXX: padded 16-byte structs. could eventually switch to u32 indices. for now, +// locality is favoured over packing. no clue whether that's the right call! +static SHUNT(struct cmeta_slice, mod_names)[MAX_MODULES] = {0}; +static SHUNT(struct cmeta_slice, mod_featdescs)[MAX_MODULES] = {0}; +static SHUNT(struct cmeta_slice, mod_gamespecific)[MAX_MODULES] = {0}; +enum { + HAS_INIT = 1, // really, this means this *is a feature*! + HAS_PREINIT = 2, + HAS_END = 4, + HAS_EVENTS = 8, + HAS_OPTDEPS = 16, // something else depends on *us* with REQUEST() + DFS_SEEING = 64, // for REQUIRE() cycle detection + DFS_SEEN = 128 +}; +static u8 mod_flags[MAX_MODULES] = {0}; +static SHUNT(struct list_chunk, mod_needs)[MAX_MODULES] = {0}; +static SHUNT(struct list_chunk, mod_wants)[MAX_MODULES] = {0}; +static SHUNT(struct list_chunk, mod_gamedata)[MAX_MODULES] = {0}; +static SHUNT(struct list_chunk, mod_globals)[MAX_MODULES] = {0}; +static int nmods = 1; + +static s16 feat_initorder[MAX_MODULES]; +static int nfeatures = 0; + +#define MAX_CVARS 8192 +#define MAX_CCMDS MAX_CVARS +static SHUNT(struct cmeta_slice, cvar_names)[MAX_CVARS]; +static SHUNT(struct cmeta_slice, ccmd_names)[MAX_CCMDS]; +static SHUNT(s16, cvar_feats)[MAX_CVARS]; +static SHUNT(s16, ccmd_feats)[MAX_CVARS]; +static SHUNT(u8, cvar_flags)[MAX_CVARS]; +static SHUNT(u8, ccmd_flags)[MAX_CVARS]; +static int ncvars = 1, nccmds = 1; +DEF_NEW(s16, cvar_new, ncvars, MAX_CVARS, "cvar entries") +DEF_NEW(s16, ccmd_new, nccmds, MAX_CCMDS, "ccmd entries") + +#define MAX_EVENTS 512 +static SHUNT(struct cmeta_slice, event_names)[MAX_EVENTS]; +static SHUNT(s16, event_owners)[MAX_EVENTS]; +static SHUNT(struct list_chunk, event_handlers)[MAX_EVENTS] = {0}; +static SHUNT(struct list_chunkhdr *, event_handlers_tails)[MAX_EVENTS] = {0}; +static SHUNT(struct list_chunk, event_params)[MAX_EVENTS] = {0}; +// XXX: would simply things a little if we could segregate the regular event and +// predicate arrays, but because of how the event.h API is currently set up, +// HANDLE_EVENT doesn't give context for what kind of event something is, so we +// can't do that in a single pass (right now we create placeholder event entries +// and check at the end to make sure we didn't miss one). fixing this is tricky! +static SHUNT(bool, event_predicateflags)[MAX_EVENTS] = {0}; +static int nevents = 1; +DEF_NEW(s16, event_new, nevents, MAX_EVENTS, "event entries") + +// a "crit-nybble tree" (see also: djb crit-bit trees) +struct radix { s16 children[16], critpos; }; +static SHUNT(struct radix, radices)[MAX_MODULES * 2 + MAX_EVENTS]; +static int nradices = 1; // also reserve a null value + +// NOTE: this will never fail, as node count is bounded by modules * 2 + events +static inline s16 radix_new(void) { return nradices++; } + +static int matchlen(const char *s1, const char *s2, int len, bool ignorecase) { + uchar c1, c2; + for (int i = 0; i < len; ++i) { + c1 = s1[i]; c2 = s2[i]; + if (ignorecase) { + c1 |= (c1 >= 'A' && c1 <= 'Z') << 5; // if A-Z, |= 32 -> a-z + c2 |= (c2 >= 'A' && c2 <= 'Z') << 5; // " + } + int diff = c1 ^ c2; + if (diff) return (i << 1) | (diff < 16); + } + return (len << 1); +} +static inline int radixbranch(const char *s, int i, bool ignorecase) { + uchar c = s[i >> 1]; + if (ignorecase) c |= (c >= 'A' && c <= 'Z') << 5; + return c >> ((~i & 1) << 2) & 0xF; +} + +/* + * Tries to insert a string index into a radix/crit-nybble/whatever trie. + * Does not actually put the index in; that way an item can be allocated only + * after determining that it doesn't already exist. Callers have to check + * ret.isnew and act accordingly. + */ +static struct radix_insert_ret { + bool isnew; + union { + s16 *strp; /* If isnew, caller must do `*ret.strp = -stridx;`. */ + s16 stridx; /* If !isnew, `stridx` points at the existing string. */ + }; +} radix_insert(s16 *nodep, const struct cmeta_slice *strlist, + const char *s, int len, bool ignorecase) { + // special case: an empty tree is just null; replace it with a leaf node + if (!*nodep) return (struct radix_insert_ret){true, nodep}; + s16 *insp = nodep; + for (s16 cur;;) { + cur = *nodep; + assume(cur); + if (cur < 0) { + // once we find an existing leaf node, we have to compare our string + // against it to find the critical position (i.e. the point where + // the strings differ) and then insert a new node at that point. + const struct cmeta_slice existing = strlist[-cur]; + int ml; + if (existing.len == len) { + ml = matchlen(existing.s, s, len, ignorecase); + if (ml == len << 1) { + return (struct radix_insert_ret){false, .stridx = -cur}; + } + } + else { + // ugh this is a little inelegant, can we think of a better way? + ml = matchlen(existing.s, s, + len < existing.len ? len : existing.len, ignorecase); + } + int oldbranch = radixbranch(existing.s, ml, ignorecase); + int newbranch = radixbranch(s, ml, ignorecase); + assume(oldbranch != newbranch); + for (;;) { + // splice in front of an existing string *or* an empty slot. + if (*insp <= 0) break; + struct radix *r = radices + *insp; + if (r->critpos > ml) break; + insp = r->children + radixbranch(s, r->critpos, ignorecase); + } + s16 new = radix_new(); + radices[new].critpos = ml; + radices[new].children[oldbranch] = *insp; + s16 *strp = radices[new].children + newbranch; + *insp = new; + return (struct radix_insert_ret){true, strp}; + } + // try to take the exact path to match common prefixes, but otherwise + // just pick any path that lets us find a string to compare with. + // we always have to compare against an existing string to determine the + // exact correct point at which to insert a new node. + int branch = 0; + if_hot (radices[cur].critpos <= len << 1) { + branch = radixbranch(s, radices[cur].critpos, ignorecase); + } + while (!radices[cur].children[branch]) branch = (branch + 1) & 15; + nodep = radices[cur].children + branch; + } +} + +/* + * Inserts an entry that's already in a string list. Can be used for + * dupe-checking after appending a new entry. Returns false on duplicate + * entries. + */ +static bool radix_insertidx(s16 *nodep, const struct cmeta_slice *strlist, + s16 stridx, bool ignorecase) { + const char *s = strlist[stridx].s; int len = strlist[stridx].len; + struct radix_insert_ret r = radix_insert(nodep, strlist, s, len, ignorecase); + if (r.isnew) *r.strp = -stridx; + return r.isnew; +} + +/* + * Returns the string index of an existing entry matching `s` and `len`. + * Returns 0 (the reserved null index) if an entry does not exist. + */ +static s16 radix_lookup(s16 node, const struct cmeta_slice *strlist, + const char *s, int len, bool ignorecase) { + while (node) { + if (node < 0) { + const struct cmeta_slice matched = strlist[-node]; + if (matched.len != len) return 0; + if (matchlen(matched.s, s, len, ignorecase) == len << 1) { + return -node; + } + return 0; + } + if (radices[node].critpos >= len << 1) return 0; + int branch = radixbranch(s, radices[node].critpos, ignorecase); + node = radices[node].children[branch]; + } + return 0; +} + +static inline void handle(s16 mod, s16 mods, s16 *featdescs, s16 *events, + const os_char *file, const struct cmeta *cm) { + bool isfeat = false; + const char *needfeat = 0; + bool canpreinit = true, haspreinit = false, hasinit = false; + struct list_chunkhdr *tail_needs = &mod_needs[mod].hdr; + struct list_chunkhdr *tail_wants = &mod_wants[mod].hdr; + struct list_chunkhdr *tail_gamedata = &mod_gamedata[mod].hdr; + struct list_chunkhdr *tail_globals = &mod_globals[mod].hdr; + for (u32 i = 0; i != cm->nitems; ++i) { + switch_exhaust_enum(cmeta_item, cm->itemtypes[i]) { + case CMETA_ITEM_DEF_CVAR: + if (!cmeta_nparams(cm, i)) { + diefile(2, file, cmeta_line(cm, i), + "cvar macro missing required parameter"); + } + int flags = cmeta_flags_cvar(cm, i); + s16 idx = cvar_new(); + cvar_flags[idx] = flags; + // NOTE: always hooking cvar/ccmd up to a feat entry for + // GAMESPECIFIC checks, even if it's not a DEF_FEAT_*. + // If a module doesn't declare FEATURE, its cvars/ccmds will + // end up getting initialised/registered unconditionally. + cvar_feats[idx] = mod; + if (flags & CMETA_CVAR_FEAT) needfeat = "feature cvar defined"; + cmeta_param_foreach (name, cm, i) { + cvar_names[idx] = name; + break; // ignore subsequent args + } + break; + case CMETA_ITEM_DEF_CCMD: + if (!cmeta_nparams(cm, i)) { + diefile(2, file, cmeta_line(cm, i), + "ccmd macro missing required parameter"); + } + flags = cmeta_flags_ccmd(cm, i); + if (flags & CMETA_CCMD_FEAT) { + needfeat = "feature ccmd defined"; + } + if (flags & CMETA_CCMD_PLUSMINUS) { + // split PLUSMINUS entries in two; makes stuff easier later. + flags &= ~CMETA_CCMD_PLUSMINUS; + idx = ccmd_new(); + ccmd_flags[idx] = flags; + ccmd_feats[idx] = mod; + cmeta_param_foreach (name, cm, i) { + char *s = arena + arena_new(5 + name.len); + memcpy(s, "PLUS_", 5); + memcpy(s + 5, name.s, name.len); + ccmd_names[idx] = (struct cmeta_slice){s, 5 + name.len}; + break; + } + idx = ccmd_new(); + ccmd_flags[idx] = flags; + ccmd_feats[idx] = mod; + cmeta_param_foreach (name, cm, i) { + char *s = arena + arena_new(6 + name.len); + memcpy(s, "MINUS_", 6); + memcpy(s + 6, name.s, name.len); + ccmd_names[idx] = (struct cmeta_slice){s, 6 + name.len}; + break; + } + } + else { + idx = ccmd_new(); + ccmd_flags[idx] = flags; + ccmd_feats[idx] = mod; + cmeta_param_foreach (name, cm, i) { + ccmd_names[idx] = name; + break; + } + } + break; + case CMETA_ITEM_DEF_EVENT: + if (!cmeta_nparams(cm, i)) { + diefile(2, file, cmeta_line(cm, i), + "event macro missing required parameter"); + } + flags = cmeta_flags_event(cm, i); + struct cmeta_param_iter it = cmeta_param_iter_init(cm, i); + struct cmeta_slice evname = cmeta_param_iter(&it); + struct radix_insert_ret r = radix_insert(events, event_names, + evname.s, evname.len, false); + int e; + if (r.isnew) { + e = event_new(); + *r.strp = -e; + event_names[e] = evname; + event_handlers_tails[e] = &event_handlers[e].hdr; + } + else { + e = r.stridx; + if (event_owners[e]) { + diefile(2, file, cmeta_line(cm, i), + "conflicting event definition"); + } + } + event_owners[e] = mod; + event_predicateflags[e] = !!(flags & CMETA_EVENT_ISPREDICATE); + struct list_chunkhdr *tail = &event_params[e].hdr; + for (struct cmeta_slice param; param = cmeta_param_iter(&it), + param.s;) { + list_append(&tail, param); + } + break; + case CMETA_ITEM_HANDLE_EVENT: + int nparams = cmeta_nparams(cm, i); + if (!nparams) { + diefile(2, file, cmeta_line(cm, i), + "event handler macro missing required parameter"); + } + it = cmeta_param_iter_init(cm, i); + evname = cmeta_param_iter(&it); + r = radix_insert(events, event_names, evname.s, evname.len, + false); + if (r.isnew) { + e = event_new(); + *r.strp = -e; + event_names[e] = evname; + event_handlers_tails[e] = &event_handlers[e].hdr; + } + else { + e = r.stridx; + } + list_append(event_handlers_tails + e, mod); + mod_flags[mod] |= HAS_EVENTS; + break; + case CMETA_ITEM_FEATURE: + isfeat = true; + // note: if no param given, featdesc will still be null + cmeta_param_foreach (param, cm, i) { + mod_featdescs[mod] = param; + if (!radix_insertidx(featdescs, mod_featdescs, mod, true)) { + diefile(2, file, cmeta_line(cm, i), + "duplicate feature description text"); + } + break; + } + break; + case CMETA_ITEM_REQUIRE: + if (!cmeta_nparams(cm, i)) { + diefile(2, file, cmeta_line(cm, i), + "dependency macro missing required parameter"); + } + flags = cmeta_flags_require(cm, i); + struct list_chunkhdr **tailp; + switch_exhaust(flags) { + case 0: tailp = &tail_needs; break; + case CMETA_REQUIRE_OPTIONAL: tailp = &tail_wants; break; + case CMETA_REQUIRE_GAMEDATA: tailp = &tail_gamedata; break; + case CMETA_REQUIRE_GLOBAL: tailp = &tail_globals; + } + cmeta_param_foreach(param, cm, i) { + int modflags = 0; + switch_exhaust(flags) { + case CMETA_REQUIRE_OPTIONAL: + modflags = HAS_OPTDEPS; + case 0: + canpreinit = false; + s16 depmod = radix_lookup(mods, mod_names, param.s, + param.len, false); + if (!depmod) { + fprintf(stderr, "cmeta_fatal: %" fS ":%d: " + "feature `%.*s` does not exist\n", + file, cmeta_line(cm, i), + param.len, param.s); + exit(2); + } + mod_flags[depmod] |= modflags; + list_append(tailp, depmod); + break; + case CMETA_REQUIRE_GAMEDATA: case CMETA_REQUIRE_GLOBAL: + list_append(tailp, param); + } + } + break; + case CMETA_ITEM_GAMESPECIFIC: + canpreinit = false; + if_cold (!cmeta_nparams(cm, i)) { + diefile(2, file, cmeta_line(cm, i), + "GAMESPECIFIC macro missing required parameter"); + } + needfeat = "GAMESPECIFIC specified"; + if_cold (mod_gamespecific[mod].s) { + diefile(2, file, cmeta_line(cm, i), + "conflicting GAMESPECIFIC macros"); + } + cmeta_param_foreach(param, cm, i) { + mod_gamespecific[mod] = param; + break; + } + break; + case CMETA_ITEM_INIT: + if (hasinit) { + diefile(2, file, cmeta_line(cm, i), "multiple INIT blocks"); + } + hasinit = true; + mod_flags[mod] |= HAS_INIT; + needfeat = "INIT block defined"; + break; + case CMETA_ITEM_PREINIT: + if (haspreinit) { + diefile(2, file, cmeta_line(cm, i), + "multiple PREINIT blocks"); + } + haspreinit = true; + mod_flags[mod] |= HAS_PREINIT; + needfeat = "PREINIT block defined"; + break; + case CMETA_ITEM_END: + if (mod_flags[mod] & HAS_END) { + diefile(2, file, cmeta_line(cm, i), "multiple END blocks"); + } + mod_flags[mod] |= HAS_END; + needfeat = "END block defined"; + } + } + if (needfeat && !isfeat) { + fprintf(stderr, "gluegen: fatal: %" fS ": %s without FEATURE()", file, + needfeat); + exit(2); + } + if (isfeat && !hasinit) { + diefile(2, file, 0, "feature is missing INIT block"); + } + if (!canpreinit && haspreinit) { + diefile(2, file, 0, "cannot use dependencies along with PREINIT"); + } +} + +static int dfs(s16 mod, bool first); +static int dfs_inner(s16 mod, s16 dep, bool first) { + if (!(mod_flags[dep] & HAS_INIT)) { + fprintf(stderr, "gluegen: fatal: feature `%.*s` tried to depend on " + "non-feature module `%.*s`\n", + mod_names[mod].len, mod_names[mod].s, + mod_names[dep].len, mod_names[dep].s); + exit(2); + } + switch (dfs(dep, false)) { + // unwind the call stack by printing each node in the dependency cycle. + // ASCII arrows are kind of ugly here but windows' CRT handles unicode + // *so* horrendously and I'm not in the mood to replace stdio for this + // (maybe another time!) + case 1: + fprintf(stderr, ".-> %.*s\n", + mod_names[mod].len, mod_names[mod].s); + return 2; + case 2: + fprintf(stderr, first ? "'-- %.*s\n" : "| %.*s\n", + mod_names[mod].len, mod_names[mod].s); + return 2; + } + return 0; +} +static int dfs(s16 mod, bool first) { + if (mod_flags[mod] & DFS_SEEN) return 0; + if (mod_flags[mod] & DFS_SEEING) { + fprintf(stderr, "gluegen: fatal: feature dependency cycle:\n"); + return 1; + } + mod_flags[mod] |= DFS_SEEING; + list_foreach(s16, dep, mod_needs + mod) { + if (dfs_inner(mod, dep, first)) return 2; + } + list_foreach(s16, dep, mod_wants + mod) { + if (dfs_inner(mod, dep, first)) return 2; + } + feat_initorder[nfeatures++] = mod; + mod_flags[mod] |= DFS_SEEN; // note: no need to bother unsetting SEEING. + return 0; +} + +static inline void sortfeatures(void) { + for (int i = 1; i < nmods; ++i) { + if ((mod_flags[i] & HAS_INIT) && dfs(i, true)) exit(2); + } +} + +static inline noreturn diewrite(void) { die(100, "couldn't write to file"); } +#define _(x) \ + if (fprintf(out, "%s\n", x) < 0) diewrite(); +#define F(f, ...) \ + if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +#define H_() \ + _("/* This file is autogenerated by src/build/gluegen.c. DO NOT EDIT! */") +#define H() H_() _("") + +static void recursefeatdescs(FILE *out, s16 node) { + if (node < 0) { + if (mod_featdescs[-node].s) { +F( " if (status_%.*s != FEAT_SKIP) {", + mod_names[-node].len, mod_names[-node].s) +F( " con_colourmsg(status_%.*s == FEAT_OK ? &green : &red,", + mod_names[-node].len, mod_names[-node].s) +F( " featmsgs[status_%.*s], %.*s);", + mod_names[-node].len, mod_names[-node].s, + mod_featdescs[-node].len, mod_featdescs[-node].s) +_( " }") + } + } + else if (node > 0) { + for (int i = 0; i < 16; ++i) { + recursefeatdescs(out, radices[node].children[i]); + } + } +} + +static int evargs(FILE *out, s16 i, const char *suffix) { + int j = 1; + if (fprintf(out, "(") < 0) diewrite(); + list_foreach (struct cmeta_slice, param, event_params + i) { + if (param.len == 4 && !memcmp(param.s, "void", 4)) { + // UGH, crappy special case for (void). with C23 this could be + // redundant, but I think we still want to avoid blowing up gluegen + // if someone (me?) does it the old way out of habit. + // also if someone does void, int something_else this will create + // nonsense, but that's just garbage-in-garbage-out I guess. + break; + } + else if (fprintf(out, "%stypeof(%.*s) a%d", j == 1 ? "" : ", ", + param.len, param.s, j) < 0) { + diewrite(); + } + ++j; + } + if (fputs(suffix, out) < 0) diewrite(); + return j; +} + +static int evargs_notype(FILE *out, s16 i, const char *suffix) { + int j = 1; + if (fprintf(out, "(") < 0) diewrite(); + list_foreach(struct cmeta_slice, param, event_params + i) { + if (param.len == 4 && !memcmp(param.s, "void", 4)) { + break; + } + if (fprintf(out, "%sa%d", j == 1 ? "" : ", ", j) < 0) { + diewrite(); + } + ++j; + } + if (fputs(suffix, out) < 0) diewrite(); + return j; +} + +static inline void gencode(FILE *out, s16 featdescs) { + for (int i = 1; i < nmods; ++i) { + if (mod_flags[i] & HAS_INIT) { +F( "extern int _feat_init_%.*s(void);", mod_names[i].len, mod_names[i].s) + } + if (mod_flags[i] & HAS_PREINIT) { +F( "extern int _feat_preinit_%.*s(void);", mod_names[i].len, mod_names[i].s) + } + if (mod_flags[i] & HAS_END) { +F( "extern void _feat_end_%.*s(void);", mod_names[i].len, mod_names[i].s) + } + } +_( "") +_( "static struct {") + for (int i = 1; i < nmods; ++i) { + if (mod_flags[i] & HAS_OPTDEPS) continue; + if ((mod_flags[i] & HAS_INIT) && + (mod_flags[i] & (HAS_END | HAS_EVENTS))) { +F( " bool _has_%.*s : 1;", mod_names[i].len, mod_names[i].s) + } + } + for (int i = 1; i < nmods; ++i) { + if (mod_flags[i] & HAS_PREINIT) { +F( " int preinit_%.*s : 2;", mod_names[i].len, mod_names[i].s) + } + } +_( "} feats = {0};") +_( "") + for (int i = 1; i < nmods; ++i) { + if (!(mod_flags[i] & HAS_INIT)) continue; + if (mod_flags[i] & HAS_OPTDEPS) { + // If something REQUESTS a feature, it needs to be able to query + // has_*, so make it extern. XXX: this could be bitpacked as well + // potentially however the whole struct would then have to be + // included in all the places that use feature macros, which would + // probably screw up the potential for fast incremental builds... +F( "bool has_%.*s = false;", mod_names[i].len, mod_names[i].s) + } + else if (mod_flags[i] & (HAS_END | HAS_EVENTS)) { + // mildly stupid, but easier really. paper over the difference so we + // can generate the same has_ checks elsewhere :^) +F( "#define has_%.*s (feats._has_%.*s)", + mod_names[i].len, mod_names[i].s, mod_names[i].len, mod_names[i].s) + } + } +_( "") + for (int i = 1; i < ncvars; ++i) { +F( "extern struct con_var *%.*s;", cvar_names[i].len, cvar_names[i].s); + } + for (int i = 1; i < nccmds; ++i) { +F( "extern struct con_cmd *%.*s;", ccmd_names[i].len, ccmd_names[i].s); + } +_( "") +_( "static inline void preinitfeatures(void) {") + for (int i = 1; i < nmods; ++i) { + if (mod_flags[i] & HAS_PREINIT) { +F( " feats.preinit_%.*s = _feat_preinit_%.*s();", + mod_names[i].len, mod_names[i].s, mod_names[i].len, mod_names[i].s) + } + } +_( "}") +_( "") +_( "static inline void initfeatures(void) {") + for (int i = 0; i < nfeatures; ++i) { // N.B.: this *should* be 0-indexed! + const char *else_ = ""; + s16 mod = feat_initorder[i]; + if (mod_flags[mod] & HAS_PREINIT) { +F( " s8 status_%.*s = feats.preinit_%.*s;", + mod_names[mod].len, mod_names[mod].s, + mod_names[mod].len, mod_names[mod].s) + } + else { +F( " s8 status_%.*s;", mod_names[mod].len, mod_names[mod].s) + } + if (mod_gamespecific[mod].s) { +F( " %sif (!GAMETYPE_MATCHES(%.*s)) status_%.*s = FEAT_SKIP;", else_, + mod_gamespecific[mod].len, mod_gamespecific[mod].s, + mod_names[mod].len, mod_names[mod].s) + else_ = "else "; + } + list_foreach (struct cmeta_slice, gamedata, mod_gamedata + mod) { + // this is not a *totally* ideal way of doing this, but it's easy. + // if we had some info about what gamedata was doing, we could avoid + // having to ifdef these cases and could just directly generate the + // right thing. but that'd be quite a bit of work, so... we don't! + if (mod_gamespecific[mod].s) { +F( "#ifdef _GAMES_WITH_%.*s", gamedata.len, gamedata.s) +F( " %sif (!(_gametype_tag_%.*s & _GAMES_WITH_%.*s) && !has_%.*s) {", else_, + mod_gamespecific[mod].len, mod_gamespecific[mod].s, + gamedata.len, gamedata.s, gamedata.len, gamedata.s) +F( " status_%.*s = NOGD;", mod_names[mod].len, mod_names[mod].s) +_( " }") +_( "#else") + } +F( " %sif (!has_%.*s) status_%.*s = NOGD;", else_, + gamedata.len, gamedata.s, mod_names[mod].len, mod_names[mod].s) + if (mod_gamespecific[mod].s) { +_( "#endif") + } + else_ = "else "; + } + list_foreach (struct cmeta_slice, global, mod_globals + mod) { +F( " %sif (!(%.*s)) status_%.*s = NOGLOBAL;", else_, + global.len, global.s, mod_names[mod].len, mod_names[mod].s) + else_ = "else "; + } + list_foreach (s16, dep, mod_needs + mod) { +F( " %sif (status_%.*s != FEAT_OK) status_%.*s = REQFAIL;", else_, + mod_names[dep].len, mod_names[dep].s, + mod_names[mod].len, mod_names[mod].s) + else_ = "else "; + } + if (mod_flags[mod] & (HAS_END | HAS_EVENTS | HAS_OPTDEPS)) { +F( " %sif ((status_%.*s = _feat_init_%.*s()) == FEAT_OK) has_%.*s = true;", + else_, + mod_names[mod].len, mod_names[mod].s, + mod_names[mod].len, mod_names[mod].s, + mod_names[mod].len, mod_names[mod].s) + } + else { +F( " %sstatus_%.*s = _feat_init_%.*s();", else_, + mod_names[mod].len, mod_names[mod].s, + mod_names[mod].len, mod_names[mod].s) + } + } +_( "") + for (int i = 1; i < ncvars; ++i) { + if (!(cvar_flags[i] & CMETA_CVAR_UNREG)) { + if (cvar_flags[i] & CMETA_CVAR_FEAT) { + struct cmeta_slice modname = mod_names[cvar_feats[i]]; +F( " if (status_%.*s != FEAT_SKIP) con_regvar(%.*s);", + modname.len, modname.s, cvar_names[i].len, cvar_names[i].s) +F( " else if (status_%.*s != FEAT_OK) %.*s->base.flags |= CON_HIDDEN;", + modname.len, modname.s, cvar_names[i].len, cvar_names[i].s) + } + else { +F( " con_regvar(%.*s);", cvar_names[i].len, cvar_names[i].s) + } + } + } + for (int i = 1; i < nccmds; ++i) { + if (!(ccmd_flags[i] & CMETA_CCMD_UNREG)) { + if (ccmd_flags[i] & CMETA_CCMD_FEAT) { + struct cmeta_slice modname = mod_names[ccmd_feats[i]]; +F( " if (status_%.*s == FEAT_OK) con_regcmd(%.*s);", + modname.len, modname.s, ccmd_names[i].len, ccmd_names[i].s) + } + else { +F( " con_regcmd(%.*s);", ccmd_names[i].len, ccmd_names[i].s) + } + } + } +_( "") +_( " successbanner();") +_( " struct rgba white = {255, 255, 255, 255};") +_( " struct rgba green = {128, 255, 128, 255};") +_( " struct rgba red = {255, 128, 128, 255};") +_( " con_colourmsg(&white, \"---- List of plugin features ---\\n\");"); + recursefeatdescs(out, featdescs); +_( "}") +_( "") +_( "static inline void endfeatures(void) {") + for (int i = nfeatures - 1; i >= 0; --i) { + s16 mod = feat_initorder[i]; + if (mod_flags[mod] & HAS_END) { +F( " if (has_%.*s) _feat_end_%.*s();", + mod_names[mod].len, mod_names[mod].s, + mod_names[mod].len, mod_names[mod].s) + } + } +_( "}") +_( "") +_( "static inline void freevars(void) {") + for (int i = 1; i < ncvars; ++i) { +F( " extfree(%.*s->strval);", cvar_names[i].len, cvar_names[i].s) + } +_( "}") + for (int i = 1; i < nevents; ++i) { + const char *prefix = event_predicateflags[i] ? + "bool CHECK_" : "void EMIT_"; + if (fprintf(out, "\n%s%.*s", prefix, + event_names[i].len, event_names[i].s) < 0) { + diewrite(); + } + evargs(out, i, ") {\n"); + list_foreach(s16, mod, event_handlers + i) { + const char *type = event_predicateflags[i] ? "bool" : "void"; + if (fprintf(out, "\t%s _evhandler_%.*s_%.*s", type, + mod_names[mod].len, mod_names[mod].s, + event_names[i].len, event_names[i].s) < 0) { + diewrite(); + } + evargs(out, i, ");\n"); + if (event_predicateflags[i]) { + if (mod_flags[mod] & HAS_INIT) { + if (fprintf(out, "\tif (has_%.*s && !", + mod_names[mod].len, mod_names[mod].s) < 0) { + diewrite(); + } + } + else if (fputs("\tif (!", out) < 0) { + diewrite(); + } + if (fprintf(out, "_evhandler_%.*s_%.*s", + mod_names[mod].len, mod_names[mod].s, + event_names[i].len, event_names[i].s) < 0) { + diewrite(); + } + evargs_notype(out, i, ")) return false;\n"); + } + else { + if (fputc('\t', out) < 0) diewrite(); + if ((mod_flags[mod] & HAS_INIT) && fprintf(out, "if (has_%.*s) ", + mod_names[mod].len, mod_names[mod].s) < 0) { + diewrite(); + } + if (fprintf(out, "_evhandler_%.*s_%.*s", + mod_names[mod].len, mod_names[mod].s, + event_names[i].len, event_names[i].s) < 0) { + diewrite(); + } + evargs_notype(out, i, ");\n"); + } + } + if (event_predicateflags[i]) { +_( " return true;") + } +_( "}") + } +} + +int OS_MAIN(int argc, os_char *argv[]) { + s16 modlookup = 0, featdesclookup = 0, eventlookup = 0; + if (argc > MAX_MODULES) { + die(2, "too many files passed - increase MAX_MODULES in gluegen.c!"); + } + nmods = argc; + for (int i = 1; i < nmods; ++i) { + const os_char *f = argv[i]; + int len = 0; + int lastpart = 0; + for (; f[len]; ++len) { +#ifdef _WIN32 + if (f[len] == '/' || f[len] == '\\') lastpart = len + 1; +#else + if (f[len] == '/') lastpart = len + 1; +#endif + } + if_cold (!len) die(1, "empty file path given"); + if_cold (len < lastpart) diefile(1, f, 0, "invalid file path"); + if_cold (len < 3 || memcmp(f + len - 2, OS_LIT(".c"), + 2 * ssizeof(os_char))) { + diefile(1, f, 0, "not a C source file (.c)"); + } + struct cmeta_slice modname; + // ugh. same dumb hack from compile scripts + if_cold (len - lastpart == 6 && !memcmp(f + lastpart, OS_LIT("con_.c"), + 6 * ssizeof(os_char))) { + modname.s = "con"; modname.len = 3; + } + else { + char *p = arena + arena_new(len - lastpart - 2); + // XXX: Unicode isn't real, it can't hurt you. + for (int i = lastpart, j = 0; i < len - 2; ++i, ++j) p[j] = f[i]; + modname.s = p; modname.len = len - lastpart - 2; + } + mod_names[i] = modname; + if (!radix_insertidx(&modlookup, mod_names, i, false)) { + // XXX: might have to fix this some day to handle subdirs and such. + // for now we rely on it happening not to be a problem basically lol + diefile(2, f, 0, "duplicate module name"); + } + } + for (int i = 1; i < nmods; ++i) { + struct cmeta cm = cmeta_loadfile(argv[i]); + handle(i, modlookup, &featdesclookup, &eventlookup, argv[i], &cm); + } + // double check that events are defined. the compiler would also catch this, + // but we can do it faster and with arguably more helpful error information. + for (int i = 1; i < nevents; ++i) { + if (!event_owners[i]) { + fprintf(stderr, "gluegen: fatal: undefined event %.*s\n", + event_names[i].len, event_names[i].s); + exit(2); + } + } + sortfeatures(); + + FILE *out = fopen(".build/include/glue.gen.h", "wb"); + if (!out) die(100, "couldn't open .build/include/glue.gen.h"); + H() + gencode(out, featdesclookup); + if (fflush(out)) die(100, "couldn't finish writing output"); + return 0; +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 |