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)