fix(compiler): return of a Text interp of a place handed out the borrow

- emit_return's place test matched only bare Ident/Field/Index, so
  `return "${p.content}"` (p a loop borrow) returned the part's own
  string; the caller's eventual drop freed it under the container —
  arena corruption surfacing two requests later (multipart slice)
- the return test now sees through Interp exactly as copy_place_text
  does (is_borrowed_value_t, container reads excluded); bare
  Ident/Field/Index behavior at return unchanged
- interp-borrowed-field fixture grows the return flavor (fn first),
  50 iterations exact
- gates: oop-e2e 89/0 (ASan stage), woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-20 03:32:31 +02:00
parent a5826495e9
commit ed29ccadbc
3 changed files with 27 additions and 3 deletions

View file

@ -140,8 +140,12 @@ so a Text-typed SINGLE-SEGMENT interpolation of a place
register through a `let`/assignment boundary uncopied — the binding aliased
the row's field and its overwrite freed it (release-build crash the arena
hid from ASan). It now asks `is_borrowed_value_t && not is_container_read`,
exactly `drop_fresh_text`'s place test. Pinned by
`tests/corpus/run/interp-borrowed-field`.
exactly `drop_fresh_text`'s place test. The RETURN boundary had the same
hole (`return "${p.content}"` handed the caller the part's own string —
the multipart slice's arena corruption, two requests removed from the
crash): emit_return's place test now sees through `Interp` the same way,
while bare Ident/Field/Index behavior there is unchanged. Both flavors
pinned by `tests/corpus/run/interp-borrowed-field`.
Two rules the measurements imposed, both easy to get backwards:

View file

@ -3756,7 +3756,16 @@ and emit_return (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (opt : Ast.ex
`return lv` inside `for lv in [...]` is WO-E304 and the workload's own
level classifier cannot be written at all. *)
let t =
let is_place = match e.Ast.kind with Ast.Ident _ | Ast.Field _ | Ast.Index _ -> true | _ -> false in
(* an interpolation is seen through exactly as copy_place_text sees
it: `return "${p.content}"` (p a loop borrow) handed the caller the
part's own string — the caller's drop then freed it under the
container, the multipart-400 arena corruption *)
let is_place =
match e.Ast.kind with
| Ast.Ident _ | Ast.Field _ | Ast.Index _ -> true
| Ast.Interp _ -> is_borrowed_value_t p f e && not (is_container_read e)
| _ -> false
in
let is_text = match ty_of_expr p f e with Some ty -> field_kind p ty = 3 | None -> false in
if is_place && is_text then begin
let w = alloc_temps p f e.pos 1 in

View file

@ -16,6 +16,15 @@ class Tab {
}
return out;
}
-- the RETURN flavor: an interpolation of a borrowed field handed straight
-- to the caller must be the caller's own copy, not the row's string
fn first() -> Text {
for r in self.rows {
return "${r.method}";
}
return "";
}
}
fn main() -> Int {
@ -26,6 +35,8 @@ fn main() -> Int {
while i < 50 {
let s = t.scan();
if s != "GET, POST" { print("bad: ${s} at ${i}"); return 1; }
let h = t.first();
if h != "GET" { print("bad first: ${h} at ${i}"); return 1; }
i = i + 1;
}
print("ok");