diff --git a/lang/dist/platform/elc b/lang/dist/platform/elc index d2f167f..1745eac 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 a30352c..767bb6b 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -1887,6 +1887,34 @@ el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) { return 1; } +// stdout_to_file / stdout_restore — redirect process stdout to a file and +// restore it. Used by the compiler's JS post-processing pipeline to capture +// codegen output before piping through terser / obfuscator. +#include +static int _el_saved_stdout_fd = -1; + +el_val_t stdout_to_file(el_val_t pathv) { + const char* path = EL_CSTR(pathv); + if (!path) return (el_val_t)(int64_t)-1; + fflush(stdout); + _el_saved_stdout_fd = dup(STDOUT_FILENO); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return (el_val_t)(int64_t)-1; + dup2(fd, STDOUT_FILENO); + close(fd); + return (el_val_t)(int64_t)0; +} + +el_val_t stdout_restore(void) { + if (_el_saved_stdout_fd >= 0) { + fflush(stdout); + dup2(_el_saved_stdout_fd, STDOUT_FILENO); + close(_el_saved_stdout_fd); + _el_saved_stdout_fd = -1; + } + return (el_val_t)(int64_t)0; +} + // exec_command — run a shell command, return exit code (0 = success). // Used by elb and other El tooling to invoke subprocesses. el_val_t exec_command(el_val_t cmdv) { diff --git a/lang/el-compiler/runtime/el_runtime.h b/lang/el-compiler/runtime/el_runtime.h index bcf8f59..a82a089 100644 --- a/lang/el-compiler/runtime/el_runtime.h +++ b/lang/el-compiler/runtime/el_runtime.h @@ -750,6 +750,10 @@ el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */ el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */ el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */ +/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */ +el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */ +el_val_t stdout_restore(void); /* restore process stdout to terminal */ + el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json); el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json); el_val_t trace_span_start(el_val_t name); diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 6ac69ce..2cbd90c 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -205,40 +205,42 @@ fn next_html_id() -> String { // We build them all into parts, then the caller wraps with concat chain. fn cg_html_parts(children: [Map], acc_var: String) -> String { + // Accumulate fragments into a list to avoid O(n²) string growth. + // Each append is O(1); the single str_join at the end is O(total_size). let n: Int = native_list_len(children) let i = 0 - let out = "" + let parts: [String] = native_list_empty() while i < n { let child: Map = native_list_get(children, i) let html_kind: String = child["html"] if str_eq(html_kind, "Text") { let text: String = child["text"] - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); ") } if str_eq(html_kind, "Doctype") { - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); ") } if str_eq(html_kind, "Interp") { let val_node = child["value"] let val_c: String = cg_expr(val_node) - let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ") } if str_eq(html_kind, "Raw") { let val_node = child["value"] let val_c: String = cg_expr(val_node) - let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); ") } if str_eq(html_kind, "Element") { let elem_c: String = cg_html_element_str(child, acc_var) - let out = out + elem_c + let parts = native_list_append(parts, elem_c) } if str_eq(html_kind, "Each") { let each_c: String = cg_html_each(child, acc_var) - let out = out + each_c + let parts = native_list_append(parts, each_c) } let i = i + 1 } - out + str_join(parts, "") } // Generate open-tag attribute fragments inline. @@ -247,9 +249,10 @@ fn cg_html_parts(children: [Map], acc_var: String) -> String { // Dynamic: "value" is an expr node. // Bool: no "value" field. fn cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { + // Accumulate fragments into a list to avoid O(n²) string growth. let n: Int = native_list_len(attrs) let i = 0 - let out = "" + let parts: [String] = native_list_empty() // Closing-quote snippet: EL_STR("\"") in C text. let close_q: String = "EL_STR(" + c_str_lit("\"") + ")" while i < n { @@ -262,26 +265,26 @@ fn cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { if str_eq(kind, "static") { // Static attribute: value is a raw string. let sv: String = attr["value"] - let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); " - let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ") + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); ") + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ") } else { if str_eq(kind, "dynamic") { // Dynamic attribute: value is an expr node — html_escape it. let val_node = attr["value"] let val_c: String = cg_expr(val_node) - let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " - let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " - let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ") + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ") + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ") } else { // Boolean attribute (no value): emit " name" let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")" - let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); ") } } let i = i + 1 } - out + str_join(parts, "") } // Generate code for a single element, appending into acc_var. @@ -290,19 +293,21 @@ fn cg_html_element_str(elem: Map, acc_var: String) -> String { let attrs: [Map] = elem["attrs"] let children: [Map] = elem["children"] let self_closing: Bool = elem["self_closing"] + // Accumulate into a list to avoid O(n²) string growth for deeply nested trees. + let parts: [String] = native_list_empty() // Open tag: - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); ") } else { // Close open tag: > - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); " - let out = out + cg_html_parts(children, acc_var) - let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); ") + let parts = native_list_append(parts, cg_html_parts(children, acc_var)) + let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); ") } - out + str_join(parts, "") } // Generate code for {#each list as item} ... {/each}. diff --git a/lang/elb.el b/lang/elb.el index af878e7..dd9b298 100644 --- a/lang/elb.el +++ b/lang/elb.el @@ -225,6 +225,7 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B let bname: String = basename_noext(src_path) let c_out: String = out_dir + "/" + bname + ".c" let elh_out: String = out_dir + "/" + bname + ".elh" + let err_tmp: String = "/tmp/elb-err-" + bname + ".txt" // Check if recompile needed if !file_is_newer(src_path, c_out) { @@ -234,18 +235,26 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B return true } - // elc streams C to stdout (collect mode not yet implemented); use - // shell redirection so the output lands in the file, not the terminal. - let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1" + // elc streams C to stdout; redirect stderr to a temp file so we can + // surface the actual error message on failure instead of swallowing it. + let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp println(" compile " + src_path) if dry_run { return true } let ret: Int = exec_command(cmd) if ret != 0 { + // Surface the actual compiler error from stderr + let err_msg: String = str_trim(fs_read(err_tmp)) + if !str_eq(err_msg, "") { + println(err_msg) + } + // Remove partial output so a retry starts clean + exec_command("rm -f " + c_out + " " + err_tmp) println("elb: compile failed: " + src_path) return false } + exec_command("rm -f " + err_tmp) // Move the generated .elh (written next to the source by elc) into // out_dir so that #include "module.elh" lines in the generated .c @@ -320,6 +329,20 @@ fn main() -> Void { runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c" } } + // If --runtime points to a directory, auto-locate el_runtime.c inside it. + // This lets both forms work: + // --runtime=/opt/el/el-compiler/runtime (directory form) + // --runtime=/opt/el/el-compiler/runtime/el_runtime.c (file form) + if !str_eq(runtime_path, "") { + let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file")) + if str_eq(is_dir, "dir") { + let candidate: String = runtime_path + "/el_runtime.c" + let has_file: String = str_trim(exec_capture("test -f " + candidate + " && echo yes || echo no")) + if str_eq(has_file, "yes") { + let runtime_path = candidate + } + } + } if str_eq(runtime_path, "") { println("elb: cannot locate el_runtime.c - use --runtime=PATH") exit(1)