diff --git a/dist/platform/elc b/dist/platform/elc index cffacef..1ed0940 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.c b/dist/platform/elc.c index 63c864b..c97779d 100644 --- a/dist/platform/elc.c +++ b/dist/platform/elc.c @@ -2185,7 +2185,7 @@ el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { el_val_t cond_c = cg_expr(cond); cond_c = strip_outer_parens(cond_c); emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("while (")), cond_c), EL_STR(") {"))); - cg_stmts(body, el_str_concat(indent, EL_STR(" ")), declared); + cg_stmts(body, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); emit_line(el_str_concat(indent, EL_STR("}"))); return declared; } @@ -2266,10 +2266,10 @@ el_val_t cg_if_stmt(el_val_t expr, el_val_t indent, el_val_t declared) { el_val_t cond_c = cg_expr(cond); cond_c = strip_outer_parens(cond_c); emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("if (")), cond_c), EL_STR(") {"))); - cg_stmts(then_stmts, el_str_concat(indent, EL_STR(" ")), declared); + cg_stmts(then_stmts, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); if (has_else) { emit_line(el_str_concat(indent, EL_STR("} else {"))); - cg_stmts(else_stmts, el_str_concat(indent, EL_STR(" ")), declared); + cg_stmts(else_stmts, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); } emit_line(el_str_concat(indent, EL_STR("}"))); return 0; @@ -2285,7 +2285,9 @@ el_val_t cg_for_body(el_val_t item, el_val_t list_expr, el_val_t body, el_val_t emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" el_val_t ")), len_tmp), EL_STR(" = el_list_len(")), list_tmp), EL_STR(");"))); emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" for (el_val_t ")), idx), EL_STR(" = 0; ")), idx), EL_STR(" < ")), len_tmp), EL_STR("; ")), idx), EL_STR("++) {"))); emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" el_val_t ")), item), EL_STR(" = el_list_get(")), list_tmp), EL_STR(", ")), idx), EL_STR(");"))); - cg_stmts(body, el_str_concat(indent, EL_STR(" ")), declared); + el_val_t body_decl = native_list_clone(declared); + body_decl = native_list_append(body_decl, item); + cg_stmts(body, el_str_concat(indent, EL_STR(" ")), body_decl); emit_line(el_str_concat(indent, EL_STR(" }"))); emit_line(el_str_concat(indent, EL_STR("}"))); return 0; diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index bcc7a99..daf4dac 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -223,25 +223,57 @@ el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } -/* ── List ────────────────────────────────────────────────────────────────── */ -/* - * Dynamic array header: - * int64_t capacity - * int64_t length - * el_val_t elems[] - */ +/* ── Refcounted heap objects ────────────────────────────────────────────────── + * + * ElList and ElMap carry a magic-tagged header at offset 0: + * { uint32_t magic; uint32_t refcount; ... payload ... } + * + * The magic tag distinguishes refcounted objects from raw C strings (whose + * first byte is printable ASCII < 0x80) and from small integers (which can't + * be dereferenced). el_retain / el_release sniff the magic and act only on + * matching values; everything else is a safe no-op. + * + * Both ElList and ElMap use INDIRECTION: the header is fixed-size and never + * moves. The payload arrays (elems, keys, values) live in separate heap + * allocations, so realloc-grow on append never invalidates the caller's + * pointer to the header. This is what lets us mutate-in-place safely when + * the refcount is 1 and copy-on-write when it's higher. + * + * Memory model in practice: + * Single-owner accumulator (the cg_stmts pattern) — refcount stays at 1, + * appends amortize to O(1), total memory O(N) for an N-element list. + * Multi-owner branching (the cg_if_stmt pattern) — refcount > 1, each + * append on a shared list copies, so the original is preserved for the + * else-branch. Persistent semantics where they're needed; mutation where + * they're not. */ + +#define EL_MAGIC_LIST 0xE15710A1u /* >= 0x80 in MSB so 'looks_like_string' rejects */ +#define EL_MAGIC_MAP 0xE19A704Bu typedef struct { - int64_t capacity; - int64_t length; - el_val_t elems[1]; + uint32_t magic; + uint32_t refcount; +} ElHeader; + +/* ── List ────────────────────────────────────────────────────────────────── */ + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; } ElList; static ElList* list_alloc(int64_t cap) { - ElList* lst = malloc(sizeof(ElList) + (size_t)(cap > 1 ? cap - 1 : 0) * sizeof(el_val_t)); + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } - lst->capacity = cap; + 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)); + if (!lst->elems) { fputs("el_runtime: out of memory\n", stderr); exit(1); } return lst; } @@ -275,41 +307,99 @@ el_val_t el_list_get(el_val_t listv, el_val_t index) { } el_val_t el_list_append(el_val_t listv, el_val_t elem) { - /* Always allocate a fresh list rather than realloc'ing the input. - * El callers commonly hold a stale pointer to the original list (e.g. - * cg_if_stmt passes `declared` to two successive cg_stmts calls; the - * first call may realloc the underlying block, leaving the second - * with a dangling pointer). Persistent allocation eliminates that - * whole class of use-after-free at modest memory cost. */ ElList* old = (ElList*)(uintptr_t)listv; - int64_t old_len = old ? old->length : 0; - int64_t new_cap = old_len + 1; - if (new_cap < 4) new_cap = 4; - ElList* new_lst = malloc(sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t)); - if (!new_lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } - new_lst->capacity = new_cap; - new_lst->length = old_len + 1; - if (old && old_len > 0) { - memcpy(new_lst->elems, old->elems, (size_t)old_len * sizeof(el_val_t)); + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); } - new_lst->elems[old_len] = elem; - return EL_STR(new_lst); + + /* Uniquely owned: grow the elems buffer in place. The header pointer the + * caller holds doesn't move (we only realloc the inner array). This is + * the common case in compiler accumulators, and it's amortized O(1). */ + if (old->hdr.refcount <= 1) { + 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)); + if (!grown) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + old->elems = grown; + old->capacity = new_cap; + } + old->elems[old->length++] = elem; + return listv; + } + + /* Shared: copy-on-write. The original is preserved for its other owners. */ + int64_t new_cap = old->length + 1; + if (new_cap < 4) new_cap = 4; + ElList* fresh = malloc(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)); + 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)); + } + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t listv) { + /* Shallow copy: the new ElList owns its own header and elems buffer, but + * the elements themselves are shared (which is what callers want for the + * cg_if_stmt 'declared' pattern — cloning the spine, not its contents). + * Used by codegen at scope branch points where two child scopes need to + * see the same starting set of declared names without each other's + * mutations. */ + ElList* old = (ElList*)(uintptr_t)listv; + if (!old) return el_list_empty(); + int64_t cap = old->capacity > 0 ? old->capacity : 4; + if (cap < old->length) cap = old->length; + if (cap < 4) cap = 4; + ElList* fresh = malloc(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)); + 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)); + } + return EL_STR(fresh); } /* ── Map ─────────────────────────────────────────────────────────────────── */ typedef struct { + ElHeader hdr; int64_t count; + int64_t capacity; el_val_t* keys; el_val_t* values; } ElMap; -el_val_t el_map_new(el_val_t pair_count, ...) { +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; ElMap* m = malloc(sizeof(ElMap)); if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); } - m->count = pair_count; - m->keys = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1)); - m->values = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1)); + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); va_list ap; va_start(ap, pair_count); for (int64_t i = 0; i < pair_count; i++) { @@ -317,6 +407,7 @@ el_val_t el_map_new(el_val_t pair_count, ...) { m->values[i] = va_arg(ap, el_val_t); } va_end(ap); + m->count = pair_count; return EL_STR(m); } @@ -337,21 +428,107 @@ el_val_t el_get_field(el_val_t mapv, el_val_t keyv) { return el_map_get(mapv, keyv); } -el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { - ElMap* m = as_map(mapv); +/* Internal: in-place set on a uniquely-owned map. */ +static el_val_t map_set_in_place(ElMap* m, el_val_t keyv, el_val_t value) { const char* key = EL_CSTR(keyv); - if (!m) return 0; - for (int64_t i = 0; i < m->count; i++) { - const char* k = EL_CSTR(m->keys[i]); - if (k && strcmp(k, key) == 0) { m->values[i] = value; return mapv; } + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return EL_STR(m); } + } + } + if (m->count >= m->capacity) { + int64_t new_cap = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)new_cap * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)new_cap * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + m->keys = gk; + m->values = gv; + m->capacity = new_cap; } - int64_t nc = m->count + 1; - m->keys = realloc(m->keys, sizeof(el_val_t) * (size_t)nc); - m->values = realloc(m->values, sizeof(el_val_t) * (size_t)nc); m->keys[m->count] = keyv; m->values[m->count] = value; - m->count = nc; - return mapv; + m->count++; + return EL_STR(m); +} + +el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { + ElMap* m = as_map(mapv); + if (!m) return 0; + if (m->hdr.refcount <= 1) { + return map_set_in_place(m, keyv, value); + } + /* Shared: copy then set. The original is preserved for its other owners. */ + int64_t new_cap = m->count + 1; + if (new_cap < 4) new_cap = 4; + ElMap* fresh = malloc(sizeof(ElMap)); + if (!fresh) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_MAP; + fresh->hdr.refcount = 1; + fresh->count = m->count; + fresh->capacity = new_cap; + fresh->keys = malloc((size_t)new_cap * sizeof(el_val_t)); + fresh->values = malloc((size_t)new_cap * sizeof(el_val_t)); + if (!fresh->keys || !fresh->values) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + if (m->count > 0) { + memcpy(fresh->keys, m->keys, (size_t)m->count * sizeof(el_val_t)); + memcpy(fresh->values, m->values, (size_t)m->count * sizeof(el_val_t)); + } + return map_set_in_place(fresh, keyv, value); +} + +/* ── Refcount ops ─────────────────────────────────────────────────────────── */ +/* + * Both retain and release sniff the magic header to decide whether a value + * is a refcounted heap object. For small integers, raw C strings, and any + * value whose magic word doesn't match, both functions are no-ops. This lets + * codegen emit them on every let-binding without having to track types. + * + * Safety: we filter out obvious non-pointers (small magnitudes, misaligned + * addresses) before dereferencing. For any value that passes the filter and + * lives in a mapped page, reading the first 4 bytes is safe — strings start + * with printable ASCII (< 0x80), so their magic word will never collide with + * EL_MAGIC_LIST (0xE1...) or EL_MAGIC_MAP (0xE1...). Random integers that + * happen to look like aligned heap pointers are exceedingly unlikely to land + * on a page whose first 4 bytes match either magic. */ + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; /* small ints */ + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; /* low addresses */ + if (p & 0x7) return 0; /* malloc returns 8-aligned */ + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) { + h->refcount++; + } +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); + l->hdr.magic = 0; /* poison so use-after-free is detected */ + free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); + free(m->values); + m->hdr.magic = 0; + free(m); + } + } } /* ── Batch 2/3 forward decls (defined later in JSON section) ────────────── */ @@ -4046,6 +4223,10 @@ el_val_t native_list_empty(void) { return el_list_empty(); } +el_val_t native_list_clone(el_val_t list) { + return el_list_clone(list); +} + el_val_t native_string_chars(el_val_t sv) { const char* s = EL_CSTR(sv); el_val_t result = el_list_empty(); diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index da4d968..685f642 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -94,6 +94,20 @@ el_val_t el_abs(el_val_t n); el_val_t el_max(el_val_t a, el_val_t b); el_val_t el_min(el_val_t a, el_val_t b); +/* ── Refcount (ARC) ────────────────────────────────────────────────────────── + * Lists and Maps carry a refcount. Strings and ints do not — el_retain and + * el_release are safe no-ops on non-refcounted values (they sniff a magic + * header at offset 0 and only act if the magic matches). + * + * Codegen emits these at let-binding shadowing, function entry (params), and + * function exit (locals other than the returned value). The refcount lets + * el_list_append and el_map_set mutate in place when uniquely owned (cheap) + * and copy-on-write when shared (preserves persistent semantics across + * accumulator patterns in the compiler itself). */ + +void el_retain(el_val_t v); +void el_release(el_val_t v); + /* ── List ────────────────────────────────────────────────────────────────── */ el_val_t el_list_new(el_val_t count, ...); @@ -101,6 +115,7 @@ el_val_t el_list_len(el_val_t list); el_val_t el_list_get(el_val_t list, el_val_t index); el_val_t el_list_append(el_val_t list, el_val_t elem); el_val_t el_list_empty(void); +el_val_t el_list_clone(el_val_t list); /* ── Map ─────────────────────────────────────────────────────────────────── */ @@ -320,6 +335,7 @@ el_val_t native_list_get(el_val_t list, el_val_t index); el_val_t native_list_len(el_val_t list); el_val_t native_list_append(el_val_t list, el_val_t elem); el_val_t native_list_empty(void); +el_val_t native_list_clone(el_val_t list); el_val_t native_string_chars(el_val_t s); el_val_t native_int_to_str(el_val_t n); diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index be0e65f..5406e50 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -600,7 +600,10 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") - cg_stmts(body, indent + " ", declared) + // Body lives in its own C block — clone so let-bindings inside the + // loop don't leak into the parent's `declared` list (which would make + // a sibling scope's `let x` emit assignment on an undeclared name). + cg_stmts(body, indent + " ", native_list_clone(declared)) emit_line(indent + "}") return declared } @@ -670,10 +673,15 @@ fn cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Voi let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") - cg_stmts(then_stmts, indent + " ", declared) + // Each branch gets its own clone of `declared` — variables let-bound + // inside the then/else block live only in that C scope, and must not + // leak back to the parent (or to the sibling branch) through shared + // list mutation. Cheap shallow copy; the entries (variable name strings) + // are shared. + cg_stmts(then_stmts, indent + " ", native_list_clone(declared)) if has_else { emit_line(indent + "} else {") - cg_stmts(else_stmts, indent + " ", declared) + cg_stmts(else_stmts, indent + " ", native_list_clone(declared)) } emit_line(indent + "}") } @@ -688,7 +696,12 @@ fn cg_for_body(item: String, list_expr: Map, body: [Map, indent: String, declared: [String]) -> [Strin let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") - cg_stmts(body, indent + " ", declared) + cg_stmts(body, indent + " ", native_list_clone(declared)) emit_line(indent + "}") return declared } @@ -2120,10 +2120,10 @@ fn cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Voi let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") - cg_stmts(then_stmts, indent + " ", declared) + cg_stmts(then_stmts, indent + " ", native_list_clone(declared)) if has_else { emit_line(indent + "} else {") - cg_stmts(else_stmts, indent + " ", declared) + cg_stmts(else_stmts, indent + " ", native_list_clone(declared)) } emit_line(indent + "}") } @@ -2138,7 +2138,9 @@ fn cg_for_body(item: String, list_expr: Map, body: [Map