From 53e0b99d5f230436082000cbf830c9e88ce40229 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Fri, 8 May 2026 08:21:38 -0500 Subject: [PATCH] =?UTF-8?q?fix(elc):=20add=20el=5Fmem=5Fcheck()=20memory?= =?UTF-8?q?=20guard=20=E2=80=94=20abort=20before=20OS=20OOM-kill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add el_mem_check() to el_runtime.c: reads ELC_MAX_MEM_MB (default 512), checks RSS via getrusage (macOS bytes / Linux KB normalised to MB), prints a clear diagnostic to stderr and exits(1) if exceeded. Wire it into two places: - compiler.el: upfront check at --emit-header entry point - codegen.el: per-function check in the streaming loop after each el_arena_pop, so runaway growth is caught at the earliest function boundary rather than after the machine is already dying. --- lang/el-compiler/runtime/el_runtime.c | 45 +++++++++++++++++++++++++++ lang/el-compiler/runtime/el_runtime.h | 6 ++++ lang/el-compiler/src/codegen.el | 1 + lang/el-compiler/src/compiler.el | 1 + 4 files changed, 53 insertions(+) diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index df83e95..982441a 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -42,6 +42,7 @@ #include #include #include +#include /* getrusage — memory guard */ #ifdef HAVE_CURL #include #endif @@ -5674,6 +5675,50 @@ el_val_t getpid_now(void) { return (el_val_t)getpid(); } +/* el_mem_check — self-terminating memory guard for long-running compiler runs. + * + * Call this periodically (e.g. after each function compiled) to detect runaway + * memory growth before the OS OOM-killer fires. Reads the limit from the env + * var ELC_MAX_MEM_MB (default 512 MB). If resident set size exceeds the limit, + * prints a diagnostic to stderr and exits with code 1 so the caller (elb or a + * CI script) can handle the failure gracefully instead of having the whole + * machine go down. + * + * Platform notes: + * macOS — ru_maxrss is in bytes. + * Linux — ru_maxrss is in kilobytes. + * We normalise to MB before comparing. + * + * Returns 0 always (the only non-return path is the exit() branch). + */ +el_val_t el_mem_check(void) { + /* Read limit from env; default 512 MB. */ + long limit_mb = 512; + const char *env_val = getenv("ELC_MAX_MEM_MB"); + if (env_val && *env_val) { + long v = atol(env_val); + if (v > 0) limit_mb = v; + } + + struct rusage ru; + if (getrusage(RUSAGE_SELF, &ru) != 0) return 0; /* can't read — skip check */ + + long rss_mb; +#if defined(__APPLE__) || defined(__MACH__) + /* macOS: ru_maxrss is bytes */ + rss_mb = (long)(ru.ru_maxrss / (1024L * 1024L)); +#else + /* Linux: ru_maxrss is kilobytes */ + rss_mb = (long)(ru.ru_maxrss / 1024L); +#endif + + if (rss_mb >= limit_mb) { + fprintf(stderr, "elc: memory limit exceeded (%ldMB), aborting\n", limit_mb); + exit(1); + } + return 0; +} + /* ── args() — command-line argument access ────────────────────────────────── * Compiled El programs call args() to get a list of CLI arguments. * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. diff --git a/lang/el-compiler/runtime/el_runtime.h b/lang/el-compiler/runtime/el_runtime.h index c0529ef..211c923 100644 --- a/lang/el-compiler/runtime/el_runtime.h +++ b/lang/el-compiler/runtime/el_runtime.h @@ -531,6 +531,12 @@ el_val_t parse_int(el_val_t s, el_val_t default_val); el_val_t exit_program(el_val_t code); el_val_t getpid_now(void); +/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and + * exits with code 1 if resident memory exceeds the limit. Call periodically + * during long compilation loops (e.g. after each function is compiled). + * Returns 0 when memory is within bounds. */ +el_val_t el_mem_check(void); + /* ── CGI identity ───────────────────────────────────────────────────────────── * Called at the start of main() in CGI programs (those with a `cgi {}` block). * Records the program's DHARMA identity before any other code executes. */ diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 9318d6c..917fb31 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3730,6 +3730,7 @@ fn codegen_streaming(tokens: [Any], sigs: [Map], source: String) -> cg_fn(stmt) el_release(stmt) el_arena_pop(fn_arena_mark) + el_mem_check() } } else { if is_top_level_decl(stmt) { diff --git a/lang/el-compiler/src/compiler.el b/lang/el-compiler/src/compiler.el index 5086b16..b9647bb 100644 --- a/lang/el-compiler/src/compiler.el +++ b/lang/el-compiler/src/compiler.el @@ -571,6 +571,7 @@ fn main() -> Void { // BinOp chains (e.g. checkout.el) — parse() builds O(whole-program AST) // while scan_fn_sigs_el keeps peak memory at O(tokens). if do_emit_header { + el_mem_check() let raw_source: String = fs_read(src_path) let hdr_tokens: [Any] = lex(raw_source) let hdr_sigs: [Map] = scan_fn_sigs_el(hdr_tokens)