Skip to content

codegen: any string-typed return makes a closure a string closure - #2070

Merged
nicolas-maman merged 2 commits into
mainfrom
fix/closure-mixed-string-returns
Sep 18, 2026
Merged

nicolas-maman merged 2 commits into
mainfrom
fix/closure-mixed-string-returns

Conversation

@nicolas-maman

Copy link
Copy Markdown
Collaborator

Why

aether-lang-dev/aether-ui#147 moved that tree to 0.683.0 and table_demo aborts at startup on macOS (Abort trap: 6). Reproduced natively on Windows: exit 0xC0000374 STATUS_HEAP_CORRUPTION. gdb:

#5 aether_heap_str_free (s=0x7ff7d0c9330f <fs_WALK_STOP+355> "zoe")   ← a string LITERAL
#6 _closure_fn_6 (…)  at ./ui/module.ae:4675      cell_s = _table_invoke_cell(cf, item, cj)

The table's cell callback is

callback |item: ptr, c: int| {
    p = item as *Person
    if c == 1 { return string.from_int(p.kb) }
    return p.name
}

resolve_closure_return_type types a closure by its first return site. string.from_int is extern … -> ptr, so the closure was emitted static void* _closure_fn_10(…), was not a "string closure", and neither return got the uniform-heap wrap #2054 introduced. Its caller, _table_invoke_cell(cb: fn, …) -> string { return cb(item, c) }, is classified heap-returning since #2054, so the call site sets _heap_cell_s = 1 and frees the result — the literal "zoe". 0.651 (pre-#2054) set _heap_cell_s = 0 and merely leaked the from_int string.

What

any_return_is_string: if any return site under the closure body is string-typed, the closure's C return type is const char*, so in_string_closure is set and every path goes through aether_uniform_heap_str — the literal is copied out, the AetherString is copied out by its header (the shim already handles that), the caller owns both.

Verified

  • tests/regression/test_closure_mixed_string_returns.ae: both sites, fifty rounds, the field's literal intact afterwards. Aborts under stock 0.683.0 (exit 127 / heap corruption), passes with this build.
  • The 20 existing test_*closure* / test_fn_* regression tests pass with this build (compiled + run natively on Windows against the 0.683.0 runtime).
  • aether-ui table_demo rebuilt with this compiler against the native win32 backend: runs, the driver lists 41 widgets, cells "zoe" and "100" rendered.

resolve_closure_return_type typed a closure by its FIRST return site.
string.from_int(x) is declared `-> ptr` (an AetherString behind a raw
pointer), so a closure returning it on one path and a struct's string
field on another was emitted `void*`, was not a string closure, and
none of its returns got the uniform-heap wrap of #2054 — while its
caller, a `-> string` function returning `cb(...)`, took ownership of
the result and freed it. The field path returned a literal; the caller
free()d it: STATUS_HEAP_CORRUPTION on Windows, SIGABRT on macOS, glibc
absorbed it silently. aether-ui's table cell callback has exactly that
shape, so every table in that tree aborted at startup from 0.682 on
(aether-ui#147, the 0.683 pin).

Any string-typed return site now makes the closure `const char*`, so
every path is wrapped: the literal is copied out, the AetherString is
copied out by its header, and the caller owns both.

tests/regression/test_closure_mixed_string_returns.ae drives both
sites fifty times; it aborts under the previous compiler.
@nicolas-maman
nicolas-maman merged commit 7344d85 into main Sep 18, 2026
28 checks passed
@nicolas-maman
nicolas-maman deleted the fix/closure-mixed-string-returns branch September 18, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant