kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit a0d3f7beb9a75d03bd13d3b65ffb49b1f5deb17e
parent 0a7d4b2e190eaa8b05699f2d7a9474b853a4ef7f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  9 Jun 2026 10:21:33 -0700

trace: add KIT_TRACE / KIT_LOG* developer tracing facility

A single tracing facility (include/kit/trace.h) replacing the ad-hoc
per-module KIT_TRACE printf macros: KIT_TRACE(level, ...) plus
KIT_LOGE/W/I/D/T sugar over five severity levels.

- libkit stays stateless: trace points forward (module, level, file,
  line, msg) across a weak-hook seam (kit_trace_enabled / kit_trace_emit,
  no-op fallbacks in src/core/core.c). A libkit-only link traces nothing.
- driver/env/common.c owns the hosted side: parse KIT_TRACE once into a
  cached static, substring module match + level threshold, stderr sink.
- Compiled out under NDEBUG (args unevaluated); KIT_TRACE_FORCE opts back
  in for release/bootstrap debugging.
- Module defaults to __FILE__ (path filters work); override per file with
  #define KIT_TRACE_MODULE for a short tag.

KIT_TRACE grammar: comma-separated [module=]level, e.g.
KIT_TRACE=cg=trace,coff=debug or KIT_TRACE=1 for everything.

Docs: doc/BUILD.md (Build modes) covers the NDEBUG/KIT_TRACE_FORCE tie
and the weak-override seam.

Diffstat:
Mdoc/BUILD.md | 13+++++++++++++
Mdriver/env/common.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/kit/core.h | 12++++++++----
Ainclude/kit/trace.h | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/core/core.c | 25+++++++++++++++++++++++++
5 files changed, 286 insertions(+), 4 deletions(-)

diff --git a/doc/BUILD.md b/doc/BUILD.md @@ -61,6 +61,19 @@ recorded into `build/.../.build-config` so that flipping a mode flag forces a rebuild of objects that were produced under the old flags. Build-mode concerns are kept strictly separate from host-environment concerns (next section). +`RELEASE=1`'s `-DNDEBUG` also compiles out the `KIT_TRACE` / `KIT_LOG*` +developer tracing (`include/kit/trace.h`): each trace point expands to nothing, +its arguments unevaluated. Define `KIT_TRACE_FORCE` to keep tracing in an NDEBUG +build — useful for debugging a release or bootstrap image. Otherwise tracing is +a pure runtime affordance, dormant until the `KIT_TRACE` env var selects modules +and levels (`KIT_TRACE=cg=trace,coff=debug`, or `KIT_TRACE=1` for everything, +matched as a substring against each trace point's module name, default +`__FILE__`). It rides the same weak-override seam as the rest of the hosted +boundary: `libkit.a` ships no-op `kit_trace_enabled` / `kit_trace_emit` and +holds no trace state, while the hosted `driver/env/common.c` supplies the strong +overrides that parse `KIT_TRACE` once and write to stderr — so a libkit-only +link traces nothing and pays nothing. + ## Host detection: mk/env.mk `mk/env.mk` is the **only** place in the build that branches on the host OS or diff --git a/driver/env/common.c b/driver/env/common.c @@ -261,6 +261,156 @@ void kit_debug_printf(const char* fmt, ...) { const char* kit_debug_getenv(const char* name) { return getenv(name); } +/* ---------------- tracing (KIT_TRACE) ---------------- */ + +/* Hosted side of the kit/trace.h seam: parse KIT_TRACE once, cache it, and + * answer kit_trace_enabled / format kit_trace_emit. The config is immutable + * after the first read, so the lazy init is intentionally lock-free -- + * concurrent first-callers re-parse identical data into the same cache, which + * is idempotent. Tracing is diagnostic-only and never affects compiler + * output, so this process-scoped state is the one place trace state lives. */ + +#define TRACE_MAX_SPECS 32 +#define TRACE_BUF_CAP 512 + +typedef struct TraceSpec { + const char* module; /* substring matched against the trace point's module */ + int level; /* threshold 1..5 (KitTraceLevel) */ +} TraceSpec; + +static struct { + int parsed; + int global_level; /* threshold for modules with no matching spec; 0 = off */ + int nspecs; + TraceSpec specs[TRACE_MAX_SPECS]; + char buf[TRACE_BUF_CAP]; /* owns the tokenized copy the specs point into */ +} g_trace; + +static int trace_level_from_name(const char* s, size_t n) { + if (n == 5 && memcmp(s, "error", 5) == 0) return KIT_TRACE_ERROR; + if (n == 4 && memcmp(s, "warn", 4) == 0) return KIT_TRACE_WARN; + if (n == 7 && memcmp(s, "warning", 7) == 0) return KIT_TRACE_WARN; + if (n == 4 && memcmp(s, "info", 4) == 0) return KIT_TRACE_INFO; + if (n == 5 && memcmp(s, "debug", 5) == 0) return KIT_TRACE_DEBUG; + if (n == 5 && memcmp(s, "trace", 5) == 0) return KIT_TRACE_TRACE; + return 0; +} + +/* "1" / "*" / "all" are spellings of "everything, max verbosity". */ +static int trace_is_all(const char* s, size_t n) { + return (n == 1 && (s[0] == '1' || s[0] == '*')) || + (n == 3 && memcmp(s, "all", 3) == 0); +} + +static void trace_raise_global(int lvl) { + if (lvl > g_trace.global_level) g_trace.global_level = lvl; +} + +static void trace_add_module(const char* module, int lvl) { + if (g_trace.nspecs >= TRACE_MAX_SPECS) return; + g_trace.specs[g_trace.nspecs].module = module; + g_trace.specs[g_trace.nspecs].level = lvl; + g_trace.nspecs++; +} + +static void trace_parse(void) { + const char* env = getenv("KIT_TRACE"); + size_t len, i, start; + + g_trace.parsed = 1; + g_trace.global_level = 0; + g_trace.nspecs = 0; + if (!env || !env[0]) return; + + len = strlen(env); + if (len >= TRACE_BUF_CAP) len = TRACE_BUF_CAP - 1; + memcpy(g_trace.buf, env, len); + g_trace.buf[len] = '\0'; + + /* Split on ',' in place; each token is "[module=]level" or a bare token. */ + start = 0; + for (i = 0; i <= len; ++i) { + char* tok; + size_t tlen; + char* eq; + if (i != len && g_trace.buf[i] != ',') continue; + g_trace.buf[i] = '\0'; + tok = &g_trace.buf[start]; + tlen = i - start; + start = i + 1; + if (tlen == 0) continue; + + eq = memchr(tok, '=', tlen); + if (eq) { + size_t mlen = (size_t)(eq - tok); + const char* lvls = eq + 1; + size_t llen = tlen - mlen - 1; + int lvl = trace_level_from_name(lvls, llen); + if (lvl == 0 && trace_is_all(lvls, llen)) lvl = KIT_TRACE_TRACE; + if (lvl == 0) continue; /* unknown level name: ignore the spec */ + *eq = '\0'; /* terminate the module substring in place */ + if (mlen == 0) + trace_raise_global(lvl); + else + trace_add_module(tok, lvl); + } else { + int lvl = trace_level_from_name(tok, tlen); + if (trace_is_all(tok, tlen)) lvl = KIT_TRACE_TRACE; + if (lvl != 0) + trace_raise_global(lvl); /* bare level => default for all modules */ + else + trace_add_module(tok, KIT_TRACE_TRACE); /* bare module => at TRACE */ + } + } +} + +int kit_trace_enabled(const char* module, int level) { + int threshold, i; + if (!g_trace.parsed) trace_parse(); + threshold = g_trace.global_level; + if (module) { + /* Most-verbose matching module spec wins, falling back to the global. */ + for (i = 0; i < g_trace.nspecs; ++i) + if (g_trace.specs[i].level > threshold && + strstr(module, g_trace.specs[i].module) != NULL) + threshold = g_trace.specs[i].level; + } + return threshold > 0 && level <= threshold; +} + +static char trace_level_char(int level) { + switch (level) { + case KIT_TRACE_ERROR: + return 'E'; + case KIT_TRACE_WARN: + return 'W'; + case KIT_TRACE_INFO: + return 'I'; + case KIT_TRACE_DEBUG: + return 'D'; + case KIT_TRACE_TRACE: + return 'T'; + default: + return '?'; + } +} + +void kit_trace_emit(const char* module, int level, const char* file, int line, + const char* fmt, ...) { + va_list ap; + char lc = trace_level_char(level); + if (!file) file = "?"; + /* Show the module tag unless it is just the source path (the default). */ + if (module && strcmp(module, file) != 0) + fprintf(stderr, "[%c][%s] %s:%d: ", lc, module, file, line); + else + fprintf(stderr, "[%c] %s:%d: ", lc, file, line); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + void driver_printf(const char* fmt, ...) { va_list ap; va_start(ap, fmt); diff --git a/include/kit/core.h b/include/kit/core.h @@ -22,6 +22,9 @@ #define KIT_API #endif +/* Developer tracing (KIT_TRACE / KIT_LOG*) is part of the core substrate. */ +#include <kit/trace.h> + /* Opaque handles shared across component headers. */ typedef struct KitCompiler KitCompiler; typedef struct KitCompileSession KitCompileSession; @@ -196,11 +199,12 @@ typedef struct KitTargetSpec { * resolver) so consumers read a field instead of re-deriving by identity. * Zero-initialized spec constructors that bypass kit_target_new leave these * at 0; only specs that pass through kit_target_new carry resolved values. */ - uint8_t long_size; /* sizeof(long): 8 for LP64, else 4 (LLP64/ILP32) */ - uint8_t wchar_size; /* sizeof(wchar_t): 2 on Windows, else 4 */ + uint8_t long_size; /* sizeof(long): 8 for LP64, else 4 (LLP64/ILP32) */ + uint8_t wchar_size; /* sizeof(wchar_t): 2 on Windows, else 4 */ uint8_t long_double_format; /* KitLongDoubleFormat */ - uint8_t emits_eh_frame; /* 1 when os != KIT_OS_FREESTANDING, else 0 */ - uint8_t os_version_major; /* OS major version (FreeBSD: 14/15/…); 0 = unspecified */ + uint8_t emits_eh_frame; /* 1 when os != KIT_OS_FREESTANDING, else 0 */ + uint8_t os_version_major; /* OS major version (FreeBSD: 14/15/…); 0 = + unspecified */ } KitTargetSpec; typedef struct KitTargetFeature { diff --git a/include/kit/trace.h b/include/kit/trace.h @@ -0,0 +1,90 @@ +#ifndef KIT_TRACE_H +#define KIT_TRACE_H + +/* + * Structured developer tracing. + * + * libkit emits trace points through two weak hooks (kit_trace_enabled + + * kit_trace_emit). The library ships no-op fallbacks, so a libkit-only link + * (the test harnesses, an embedder) traces nothing and pays nothing. Hosted + * binaries override the hooks in driver/env: that side owns the KIT_TRACE + * config and the stderr sink. The compiler core itself holds no trace state -- + * every KIT_TRACE site just forwards (module, level, file, line, message) + * across the seam -- so libkit stays stateless and reentrant; the only trace + * state lives on the application side, where it is parsed once from the + * immutable environment. + * + * Compile-time removal: under NDEBUG the macros expand to nothing and their + * arguments are not evaluated. Define KIT_TRACE_FORCE to keep them in an + * NDEBUG build (e.g. to debug a release / bootstrap image). + * + * Runtime filtering: the KIT_TRACE environment variable selects modules and + * levels. Each comma-separated spec is "[module=]level"; a bare token is a + * level (applies to every module) or, if it is not a level name, a module + * (enabled at TRACE). A module spec matches as a substring of the module + * name, which defaults to __FILE__, so path fragments work: + * + * KIT_TRACE=1 every module, max verbosity + * KIT_TRACE=info every module, INFO and more severe + * KIT_TRACE=cg=trace,coff=debug per-module thresholds + * KIT_TRACE=warn,cg/ir=trace default WARN; path "cg/ir" at TRACE + * + * A message at level L is emitted when its module's threshold T satisfies + * L <= T (lower value = more severe), so a DEBUG threshold shows ERROR..DEBUG + * but not TRACE. + */ + +#ifndef KIT_API +#if defined(__GNUC__) || defined(__clang__) || defined(__kit__) +#define KIT_API __attribute__((visibility("default"))) +#else +#define KIT_API +#endif +#endif + +typedef enum KitTraceLevel { + KIT_TRACE_ERROR = 1, + KIT_TRACE_WARN = 2, + KIT_TRACE_INFO = 3, + KIT_TRACE_DEBUG = 4, + KIT_TRACE_TRACE = 5, +} KitTraceLevel; + +/* The seam. libkit supplies weak fallbacks (enabled -> 0, emit -> no-op); + * hosted binaries override both. Call through the KIT_TRACE macros below + * rather than directly, so NDEBUG removal and the module/file/line capture + * happen for you. */ +KIT_API int kit_trace_enabled(const char* module, int level); +KIT_API void kit_trace_emit(const char* module, int level, const char* file, + int line, const char* fmt, ...); + +/* Per-file module identity. Defaults to the source path (so KIT_TRACE path + * filters work with zero boilerplate). A file may pick a short, stable tag by + * redefining it after its includes: + * #undef KIT_TRACE_MODULE + * #define KIT_TRACE_MODULE "cg" + * The value is a token expanded at each use site, so __FILE__ stays correct + * per file even though this default is set only once. */ +#ifndef KIT_TRACE_MODULE +#define KIT_TRACE_MODULE __FILE__ +#endif + +#if defined(NDEBUG) && !defined(KIT_TRACE_FORCE) +#define KIT_TRACE(lvl, ...) ((void)0) +#else +#define KIT_TRACE(lvl, ...) \ + do { \ + if (kit_trace_enabled(KIT_TRACE_MODULE, (lvl))) \ + kit_trace_emit(KIT_TRACE_MODULE, (lvl), __FILE__, __LINE__, \ + __VA_ARGS__); \ + } while (0) +#endif + +/* Per-level sugar. */ +#define KIT_LOGE(...) KIT_TRACE(KIT_TRACE_ERROR, __VA_ARGS__) +#define KIT_LOGW(...) KIT_TRACE(KIT_TRACE_WARN, __VA_ARGS__) +#define KIT_LOGI(...) KIT_TRACE(KIT_TRACE_INFO, __VA_ARGS__) +#define KIT_LOGD(...) KIT_TRACE(KIT_TRACE_DEBUG, __VA_ARGS__) +#define KIT_LOGT(...) KIT_TRACE(KIT_TRACE_TRACE, __VA_ARGS__) + +#endif diff --git a/src/core/core.c b/src/core/core.c @@ -28,6 +28,31 @@ const char* kit_debug_getenv(const char* name) { return NULL; } +/* Weak fallbacks for the tracing seam (kit/trace.h). libkit holds no trace + * state, so the library-side gate is always closed and the sink is a no-op; + * hosted binaries (driver/env) override both with the KIT_TRACE-driven + * implementation. */ +#if defined(__GNUC__) || defined(__clang__) || defined(__kit__) +__attribute__((weak)) +#endif +int kit_trace_enabled(const char* module, int level) { + (void)module; + (void)level; + return 0; +} + +#if defined(__GNUC__) || defined(__clang__) || defined(__kit__) +__attribute__((weak)) +#endif +void kit_trace_emit(const char* module, int level, const char* file, int line, + const char* fmt, ...) { + (void)module; + (void)level; + (void)file; + (void)line; + (void)fmt; +} + /* Weak fallback for the <assert.h> failure hook (rt/include/assert.h). kit's * own code uses compiler_panic, not C assert(), but vendored code compiled into * libkit (the lz4 codecs) references __kit_assert_fail in non-NDEBUG builds.