diff --git a/lang/dist/platform/elc b/lang/dist/platform/elc index f1cba65..5e582a3 100755 Binary files a/lang/dist/platform/elc and b/lang/dist/platform/elc differ 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 a4f5fdc..b9647bb 100644 --- a/lang/el-compiler/src/compiler.el +++ b/lang/el-compiler/src/compiler.el @@ -287,6 +287,9 @@ fn type_node_to_el(t: Map) -> String { // emit_header — write a .elh file from parsed statements. // Scans for FnDef nodes and emits 'extern fn' declarations. +// NOTE: This function requires the full AST. Prefer emit_header_from_sigs +// for the --emit-header path — it works from a token-level scan without +// building expression ASTs, avoiding OOM on large files. fn emit_header(stmts: [Map], hdr_path: String) -> Void { let n: Int = native_list_len(stmts) let i = 0 @@ -325,6 +328,32 @@ fn emit_header(stmts: [Map], hdr_path: String) -> Void { let ok: Bool = fs_write(hdr_path, content) } +// emit_header_from_sigs — write a .elh file from pre-scanned El signatures. +// Uses the output of scan_fn_sigs_el() — no full AST required. +// Peak memory is O(tokens) rather than O(whole-program AST), which prevents +// OOM on large files with HTML template bodies or deep BinOp chains. +fn emit_header_from_sigs(sigs: [Map], hdr_path: String) -> Void { + let n: Int = native_list_len(sigs) + let i: Int = 0 + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n") + while i < n { + let sig = native_list_get(sigs, i) + let kind: String = sig["kind"] + if str_eq(kind, "fn") { + let name: String = sig["name"] + let params_el: String = sig["params_el"] + let ret_el: String = sig["ret_el"] + if str_eq(ret_el, "") { let ret_el = "Any" } + let line: String = "extern fn " + name + "(" + params_el + ") -> " + ret_el + let parts = native_list_append(parts, line + "\n") + } + let i = i + 1 + } + let content: String = str_join(parts, "") + let ok: Bool = fs_write(hdr_path, content) +} + // ── Import resolution ──────────────────────────────────────────────────────── // // elc supports two forms of import: @@ -536,16 +565,20 @@ fn main() -> Void { let src_path: String = native_list_get(positional, 0) - // When --emit-header is requested, parse the source file directly - // (without inlining imports) and write out a .elh file alongside the .c. + // When --emit-header is requested, lex the source file and do a + // token-level signature scan (no full AST) to write a .elh file. + // This avoids OOM on large files with HTML template bodies or deep + // 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_stmts: [Map] = parse(hdr_tokens) + let hdr_sigs: [Map] = scan_fn_sigs_el(hdr_tokens) el_release(hdr_tokens) let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh" - emit_header(hdr_stmts, hdr_path) - el_release(hdr_stmts) + emit_header_from_sigs(hdr_sigs, hdr_path) + el_release(hdr_sigs) } let source: String = resolve_imports(src_path) diff --git a/lang/el-compiler/src/parser.el b/lang/el-compiler/src/parser.el index 1e24675..e4de3b9 100644 --- a/lang/el-compiler/src/parser.el +++ b/lang/el-compiler/src/parser.el @@ -2065,6 +2065,219 @@ fn skip_expr_to_stmt_boundary(tokens: [Any], pos: Int) -> Int { p } +// scan_type_el — read a type annotation starting at pos and return its El +// source representation as a string, plus the new position. +// Returns { "el": String, "pos": Int }. +// Handles: Ident, [Type], Map, Type?, Type (same shapes as skip_type). +fn scan_type_el(tokens: [Any], pos: Int) -> Map { + let k: String = tok_kind(tokens, pos) + // Array type: [Type] + if str_eq(k, "LBracket") { + let p: Int = pos + 1 + let inner = scan_type_el(tokens, p) + let inner_str: String = inner["el"] + let p = inner["pos"] + el_release(inner) + let p = expect(tokens, p, "RBracket") + return { "el": "[" + inner_str + "]", "pos": p } + } + // Named type (possibly generic or optional) + if str_eq(k, "Ident") { + let name: String = tok_value(tokens, pos) + let p: Int = pos + 1 + let k2: String = tok_kind(tokens, p) + if str_eq(k2, "Lt") { + // Generic params: collect until matching > + let p = p + 1 + let depth: Int = 1 + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, name + "<") + let running: Bool = true + while running { + let kk: String = tok_kind(tokens, p) + if str_eq(kk, "Eof") { + let running = false + } else { + if str_eq(kk, "Lt") { + let depth = depth + 1 + let parts = native_list_append(parts, "<") + let p = p + 1 + } else { + if str_eq(kk, "Gt") { + let depth = depth - 1 + let p = p + 1 + if depth <= 0 { + let parts = native_list_append(parts, ">") + let running = false + } else { + let parts = native_list_append(parts, ">") + } + } else { + if str_eq(kk, "Comma") { + let parts = native_list_append(parts, ", ") + let p = p + 1 + } else { + let parts = native_list_append(parts, tok_value(tokens, p)) + let p = p + 1 + } + } + } + } + } + let k3: String = tok_kind(tokens, p) + if str_eq(k3, "QuestionMark") { let p = p + 1 } + let result: String = str_join(parts, "") + el_release(parts) + return { "el": result, "pos": p } + } + // Optional marker + if str_eq(k2, "QuestionMark") { + return { "el": name + "?", "pos": p + 1 } + } + return { "el": name, "pos": p } + } + // Fallback: unknown token, treat as Any + { "el": "Any", "pos": pos + 1 } +} + +// scan_params_el — scan a parameter list `(name: Type, ...)` starting at +// position `pos` (which should point at LParen) and return the El parameter +// declaration string (e.g. "a: String, b: Int") along with the new position. +// Returns { "el": String, "pos": Int }. +// Used by scan_fn_sigs_el for --emit-header without building full AST. +fn scan_params_el(tokens: [Any], pos: Int) -> Map { + let p: Int = expect(tokens, pos, "LParen") + let parts: [String] = native_list_empty() + let going: Bool = true + while going { + let kk: String = tok_kind(tokens, p) + if str_eq(kk, "RParen") { + let going = false + } else { + if str_eq(kk, "Eof") { + let going = false + } else { + let pname: String = tok_value(tokens, p) + let p = p + 1 + let p = expect(tokens, p, "Colon") + let tr = scan_type_el(tokens, p) + let ptype: String = tr["el"] + let p = tr["pos"] + el_release(tr) + let parts = native_list_append(parts, pname + ": " + ptype) + let k2: String = tok_kind(tokens, p) + if str_eq(k2, "Comma") { + let p = p + 1 + } + } + } + } + let p = expect(tokens, p, "RParen") + let el_str: String = str_join(parts, ", ") + el_release(parts) + { "el": el_str, "pos": p } +} + +// scan_fn_sigs_el — lightweight token-level pre-scan for --emit-header. +// +// Like scan_fn_sigs but captures El-style type strings instead of C types. +// Only records fn/extern_fn entries (header generation ignores lets/blocks). +// +// Descriptor shape: +// { "kind": "fn"|"extern_fn", "name": String, +// "params_el": String, <- El param list, e.g. "a: String, b: Int" +// "ret_el": String } <- El return type, e.g. "String" or "Void" +// +// Peak memory: O(tokens) with no expression AST allocation. +fn scan_fn_sigs_el(tokens: [Any]) -> [Map] { + let total: Int = native_list_len(tokens) / 2 + let sigs: [Map] = native_list_empty() + let pos: Int = 0 + let going: Bool = true + while going { + if pos >= total { + let going = false + } else { + let k: String = tok_kind(tokens, pos) + if str_eq(k, "Eof") { + let going = false + } else { + // --- fn definition --- + if str_eq(k, "Fn") { + let p: Int = pos + 1 + let name: String = tok_value(tokens, p) + let p = p + 1 + let pr = scan_params_el(tokens, p) + let params_el: String = pr["el"] + let p = pr["pos"] + el_release(pr) + // read return type + let ret_el: String = "Any" + let k2: String = tok_kind(tokens, p) + if str_eq(k2, "Arrow") { + let p = p + 1 + let tr = scan_type_el(tokens, p) + let ret_el = tr["el"] + let p = tr["pos"] + el_release(tr) + } + // skip body + let k3: String = tok_kind(tokens, p) + if str_eq(k3, "LBrace") { + let p = skip_to_rbrace(tokens, p) + } + if !str_eq(name, "main") { + let sigs = native_list_append(sigs, { + "kind": "fn", + "name": name, + "params_el": params_el, + "ret_el": ret_el + }) + } + let pos = p + } else { + // --- extern fn --- + if str_eq(k, "Extern") { + let p: Int = pos + 1 + let k2: String = tok_kind(tokens, p) + if str_eq(k2, "Fn") { + let p = p + 1 + let name: String = tok_value(tokens, p) + let p = p + 1 + let pr = scan_params_el(tokens, p) + let params_el: String = pr["el"] + let p = pr["pos"] + el_release(pr) + let ret_el: String = "Any" + let k3: String = tok_kind(tokens, p) + if str_eq(k3, "Arrow") { + let p = p + 1 + let tr = scan_type_el(tokens, p) + let ret_el = tr["el"] + let p = tr["pos"] + el_release(tr) + } + let sigs = native_list_append(sigs, { + "kind": "extern_fn", + "name": name, + "params_el": params_el, + "ret_el": ret_el + }) + let pos = p + } else { + let pos = pos + 1 + } + } else { + // Let, Cgi, Service, Import, Type, Enum, From — skip to boundary. + let p: Int = pos + 1 + let p = skip_expr_to_stmt_boundary(tokens, p) + let pos = p + }}} + } + } + sigs +} + // scan_params_c — scan a parameter list `(name: Type, ...)` starting at // position `pos` (which should point at LParen) and return the C parameter // declaration string along with the new position.