fix: codegen O(n²) HTML memory leak + elb stderr surface + runtime dir path
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -1887,6 +1887,34 @@ el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) {
|
|||||||
return 1;
|
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 <fcntl.h>
|
||||||
|
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).
|
// exec_command — run a shell command, return exit code (0 = success).
|
||||||
// Used by elb and other El tooling to invoke subprocesses.
|
// Used by elb and other El tooling to invoke subprocesses.
|
||||||
el_val_t exec_command(el_val_t cmdv) {
|
el_val_t exec_command(el_val_t cmdv) {
|
||||||
|
|||||||
@@ -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(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) */
|
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_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 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);
|
el_val_t trace_span_start(el_val_t name);
|
||||||
|
|||||||
@@ -205,40 +205,42 @@ fn next_html_id() -> String {
|
|||||||
// We build them all into parts, then the caller wraps with concat chain.
|
// We build them all into parts, then the caller wraps with concat chain.
|
||||||
|
|
||||||
fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
fn cg_html_parts(children: [Map<String, Any>], 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 n: Int = native_list_len(children)
|
||||||
let i = 0
|
let i = 0
|
||||||
let out = ""
|
let parts: [String] = native_list_empty()
|
||||||
while i < n {
|
while i < n {
|
||||||
let child: Map<String, Any> = native_list_get(children, i)
|
let child: Map<String, Any> = native_list_get(children, i)
|
||||||
let html_kind: String = child["html"]
|
let html_kind: String = child["html"]
|
||||||
if str_eq(html_kind, "Text") {
|
if str_eq(html_kind, "Text") {
|
||||||
let text: String = child["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") {
|
if str_eq(html_kind, "Doctype") {
|
||||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); "
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); ")
|
||||||
}
|
}
|
||||||
if str_eq(html_kind, "Interp") {
|
if str_eq(html_kind, "Interp") {
|
||||||
let val_node = child["value"]
|
let val_node = child["value"]
|
||||||
let val_c: String = cg_expr(val_node)
|
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") {
|
if str_eq(html_kind, "Raw") {
|
||||||
let val_node = child["value"]
|
let val_node = child["value"]
|
||||||
let val_c: String = cg_expr(val_node)
|
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") {
|
if str_eq(html_kind, "Element") {
|
||||||
let elem_c: String = cg_html_element_str(child, acc_var)
|
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") {
|
if str_eq(html_kind, "Each") {
|
||||||
let each_c: String = cg_html_each(child, acc_var)
|
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
|
let i = i + 1
|
||||||
}
|
}
|
||||||
out
|
str_join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate open-tag attribute fragments inline.
|
// Generate open-tag attribute fragments inline.
|
||||||
@@ -247,9 +249,10 @@ fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
|||||||
// Dynamic: "value" is an expr node.
|
// Dynamic: "value" is an expr node.
|
||||||
// Bool: no "value" field.
|
// Bool: no "value" field.
|
||||||
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
||||||
|
// Accumulate fragments into a list to avoid O(n²) string growth.
|
||||||
let n: Int = native_list_len(attrs)
|
let n: Int = native_list_len(attrs)
|
||||||
let i = 0
|
let i = 0
|
||||||
let out = ""
|
let parts: [String] = native_list_empty()
|
||||||
// Closing-quote snippet: EL_STR("\"") in C text.
|
// Closing-quote snippet: EL_STR("\"") in C text.
|
||||||
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
|
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
|
||||||
while i < n {
|
while i < n {
|
||||||
@@ -262,26 +265,26 @@ fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
|||||||
if str_eq(kind, "static") {
|
if str_eq(kind, "static") {
|
||||||
// Static attribute: value is a raw string.
|
// Static attribute: value is a raw string.
|
||||||
let sv: String = attr["value"]
|
let sv: String = attr["value"]
|
||||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
|
let parts = native_list_append(parts, 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 parts = native_list_append(parts, 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 + ", " + close_q + "); ")
|
||||||
} else {
|
} else {
|
||||||
if str_eq(kind, "dynamic") {
|
if str_eq(kind, "dynamic") {
|
||||||
// Dynamic attribute: value is an expr node — html_escape it.
|
// Dynamic attribute: value is an expr node — html_escape it.
|
||||||
let val_node = attr["value"]
|
let val_node = attr["value"]
|
||||||
let val_c: String = cg_expr(val_node)
|
let val_c: String = cg_expr(val_node)
|
||||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ")
|
||||||
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 + ")); ")
|
||||||
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 + ", " + close_q + "); ")
|
||||||
} else {
|
} else {
|
||||||
// Boolean attribute (no value): emit " name"
|
// Boolean attribute (no value): emit " name"
|
||||||
let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_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
|
let i = i + 1
|
||||||
}
|
}
|
||||||
out
|
str_join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate code for a single element, appending into acc_var.
|
// Generate code for a single element, appending into acc_var.
|
||||||
@@ -290,19 +293,21 @@ fn cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
|
|||||||
let attrs: [Map<String, Any>] = elem["attrs"]
|
let attrs: [Map<String, Any>] = elem["attrs"]
|
||||||
let children: [Map<String, Any>] = elem["children"]
|
let children: [Map<String, Any>] = elem["children"]
|
||||||
let self_closing: Bool = elem["self_closing"]
|
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: <tagname
|
// Open tag: <tagname
|
||||||
let out = acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); "
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); ")
|
||||||
let out = out + cg_html_attrs_str(attrs, acc_var)
|
let parts = native_list_append(parts, cg_html_attrs_str(attrs, acc_var))
|
||||||
if self_closing {
|
if self_closing {
|
||||||
// Self-closing void element: />
|
// Self-closing void element: />
|
||||||
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 {
|
} else {
|
||||||
// Close open tag: >
|
// Close 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(\">\")); ")
|
||||||
let out = out + cg_html_parts(children, acc_var)
|
let parts = native_list_append(parts, cg_html_parts(children, acc_var))
|
||||||
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); "
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); ")
|
||||||
}
|
}
|
||||||
out
|
str_join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate code for {#each list as item} ... {/each}.
|
// Generate code for {#each list as item} ... {/each}.
|
||||||
|
|||||||
+26
-3
@@ -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 bname: String = basename_noext(src_path)
|
||||||
let c_out: String = out_dir + "/" + bname + ".c"
|
let c_out: String = out_dir + "/" + bname + ".c"
|
||||||
let elh_out: String = out_dir + "/" + bname + ".elh"
|
let elh_out: String = out_dir + "/" + bname + ".elh"
|
||||||
|
let err_tmp: String = "/tmp/elb-err-" + bname + ".txt"
|
||||||
|
|
||||||
// Check if recompile needed
|
// Check if recompile needed
|
||||||
if !file_is_newer(src_path, c_out) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// elc streams C to stdout (collect mode not yet implemented); use
|
// elc streams C to stdout; redirect stderr to a temp file so we can
|
||||||
// shell redirection so the output lands in the file, not the terminal.
|
// surface the actual error message on failure instead of swallowing it.
|
||||||
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1"
|
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp
|
||||||
println(" compile " + src_path)
|
println(" compile " + src_path)
|
||||||
|
|
||||||
if dry_run { return true }
|
if dry_run { return true }
|
||||||
|
|
||||||
let ret: Int = exec_command(cmd)
|
let ret: Int = exec_command(cmd)
|
||||||
if ret != 0 {
|
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)
|
println("elb: compile failed: " + src_path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
exec_command("rm -f " + err_tmp)
|
||||||
|
|
||||||
// Move the generated .elh (written next to the source by elc) into
|
// Move the generated .elh (written next to the source by elc) into
|
||||||
// out_dir so that #include "module.elh" lines in the generated .c
|
// 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"
|
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, "") {
|
if str_eq(runtime_path, "") {
|
||||||
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
|
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user