From edafd8cce8da0749683bf2af3ab10e9cb7dcbae2 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 21:33:20 -0500 Subject: [PATCH 1/2] runtime: math_log is base-10, not natural log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } Both were natural log, so math_log and math_ln were the same function. log10(100) returned 4.605 instead of 2. Three sources already agreed it should be base-10 and were being contradicted by this one line: - runtime/math.el:55 "// math_log — base-10 logarithm." - el_seed.c:1278 __log_f -> log10() (the path math.el actually calls) - tests/native/test_math.el:133 asserts log10(100) == 2 FOUND BY THE NEW TEST FRAMEWORK ON ITS FIRST RUN (el #133). The assertion had been sitting in the suite the whole time; nothing could report it. The old harness printed "N passed, M failed" with no per-test detail, and half the suites were not compiling at all — so a failing assertion in a suite nobody could run was indistinguishable from no failure. That is the entire argument for the framework, demonstrated on day one: this is not a bug the framework introduced, it is a bug the framework made VISIBLE. Verified: tests/native/test_math.el goes 12/13 -> 13/13, math-log passing. --- lang/runtime/el_runtime.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 264fe7e..dde1acd 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -5240,7 +5240,12 @@ el_val_t str_to_float(el_val_t s) { /* ── Math (Float-aware) ──────────────────────────────────────────────────── */ el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } -el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +/* base-10, matching runtime/math.el's documented contract ("math_log — base-10 + * logarithm") and el_seed.c's __log_f. This returned NATURAL log, so math_log + * and math_ln were the same function: log10(100) gave 4.605 instead of 2. + * Caught by tests/native/test_math.el on the new framework's first run — the + * assertion existed all along, the suite just had no way to report it. */ +el_val_t math_log(el_val_t f) { return el_from_float(log10(el_to_float(f))); } el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } From a8908908dfb3feeb1aed665c088151b97fafafb7 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 21:36:52 -0500 Subject: [PATCH 2/2] runtime: count container allocations too, not just strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit el #131 instrumented the four string allocators, which meant list- and map-heavy code reported ZERO allocations — a benchmark over lists would have been fitted against a flat line and passed anything. Caught during framework work: a "linear" specimen read 0 allocs until it was rewritten to allocate strings. A gate is only as good as its blind spots are small, and a signal that silently reads zero is worse than no signal: it produces a confident pass. Now counted at every container allocation — ElList and ElMap bodies, their backing arrays, the copy-on-write clones, and the realloc growth path. Verified on an append loop (n = 100..800): allocs 7, 8, 9, 10 +1 per doubling = O(log n) reallocations bytes 2048, 4096, 8192, 16384 exactly 2x per doubling = O(n) Both curves are what correct amortized growth should look like, and both read zero before this change. Known remaining scope, stated rather than left implicit: these counters cover the runtime's own allocations. They do not see malloc inside engram_*.c or libcurl, which is correct — the gate is for El-level complexity, not for third-party memory behaviour. --- lang/runtime/el_runtime.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index dde1acd..965b891 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -476,12 +476,14 @@ typedef struct { static ElList* list_alloc(int64_t cap) { if (cap < 4) cap = 4; ElList* lst = malloc(sizeof(ElList)); + _el_alloc_count++; _el_alloc_bytes += sizeof(ElList); if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } lst->hdr.magic = EL_MAGIC_LIST; lst->hdr.refcount = 1; lst->length = 0; lst->capacity = cap; lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + _el_alloc_count++; _el_alloc_bytes += (size_t)cap * sizeof(el_val_t); if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } return lst; } @@ -531,6 +533,7 @@ el_val_t el_list_append(el_val_t listv, el_val_t elem) { if (old->length >= old->capacity) { int64_t new_cap = old->capacity > 0 ? old->capacity * 2 : 4; el_val_t* grown = realloc(old->elems, (size_t)new_cap * sizeof(el_val_t)); + _el_alloc_count++; _el_alloc_bytes += (size_t)new_cap * sizeof(el_val_t); if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } old->elems = grown; old->capacity = new_cap; @@ -543,12 +546,14 @@ el_val_t el_list_append(el_val_t listv, el_val_t elem) { int64_t new_cap = old->length + 1; if (new_cap < 4) new_cap = 4; ElList* fresh = malloc(sizeof(ElList)); + _el_alloc_count++; _el_alloc_bytes += sizeof(ElList); if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } fresh->hdr.magic = EL_MAGIC_LIST; fresh->hdr.refcount = 1; fresh->length = old->length + 1; fresh->capacity = new_cap; fresh->elems = malloc((size_t)new_cap * sizeof(el_val_t)); + _el_alloc_count++; _el_alloc_bytes += (size_t)new_cap * sizeof(el_val_t); if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } if (old->length > 0) { memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); @@ -570,12 +575,14 @@ el_val_t el_list_clone(el_val_t listv) { if (cap < old->length) cap = old->length; if (cap < 4) cap = 4; ElList* fresh = malloc(sizeof(ElList)); + _el_alloc_count++; _el_alloc_bytes += sizeof(ElList); if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } fresh->hdr.magic = EL_MAGIC_LIST; fresh->hdr.refcount = 1; fresh->length = old->length; fresh->capacity = cap; fresh->elems = malloc((size_t)cap * sizeof(el_val_t)); + _el_alloc_count++; _el_alloc_bytes += (size_t)cap * sizeof(el_val_t); if (!fresh->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } if (old->length > 0) { memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); @@ -596,6 +603,7 @@ typedef struct { static ElMap* map_alloc(int64_t cap) { if (cap < 4) cap = 4; ElMap* m = malloc(sizeof(ElMap)); + _el_alloc_count++; _el_alloc_bytes += sizeof(ElMap); if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } m->hdr.magic = EL_MAGIC_MAP; m->hdr.refcount = 1; @@ -671,6 +679,7 @@ el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { int64_t new_cap = m->count + 1; if (new_cap < 4) new_cap = 4; ElMap* fresh = malloc(sizeof(ElMap)); + _el_alloc_count++; _el_alloc_bytes += sizeof(ElMap); if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } fresh->hdr.magic = EL_MAGIC_MAP; fresh->hdr.refcount = 1;