Skip to content

fix: emit lifetime markers for lowered call temporaries - #1803

Merged
ghaith merged 2 commits into
masterfrom
fix/lowered-temp-stack-usage
Jul 28, 2026
Merged

fix: emit lifetime markers for lowered call temporaries#1803
ghaith merged 2 commits into
masterfrom
fix/lowered-temp-stack-usage

Conversation

@ghaith

@ghaith ghaith commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

Calling a function that returns an aggregate (string, struct) allocates a caller-side temporary to hold the result. Each call site gets its own temporary sized to the declared return type — STRING[2048] for every stdlib string function (CONCAT, *_TO_STRING, …) — with no lifetime information attached, so LLVM keeps every slot alive for the whole function. A body with ~50 string operations already needs >100 KB of frame and overflows small task stacks at function entry.

Fix

Emit llvm.lifetime.start/end around each lowered statement's call temporaries, so LLVM's stack coloring can overlap slots whose lifetimes don't intersect. The frame then grows with the deepest single statement instead of the number of call sites: a string-heavy test body drops from ~111 KB to ~10 KB of frame at the default optimization level.

Temporaries whose address escapes their statement must not be marked dead at statement end — stack coloring could hand their slot to a later statement while a pointer still refers to it. These are pinned (Allocation::statement_scoped = false, no markers): ADR/REF arguments, REF= right-hand sides, and interface fat-pointer captures (already wrapped in ADR by the polymorphism lowering). Loop bookkeeping temporaries keep their function-long storage since their values cross iterations.

Scope

This only benefits builds where the LLVM backend optimizes: stack coloring is a machine pass that does not run at -Onone, so unoptimized builds keep their current frame sizes. Planned follow-ups (separate PRs):

  • optimisation-level controls so that builds which want a debug-friendly experience can still run selected backend optimisations such as stack coloring
  • result buffers are still fully zero-initialized at every call site; marking aggregate out-parameters writeonly would let LLVM eliminate those memsets

Testing

  • codegen snapshot tests for marker placement, including nested calls (start/start … end/end) in string_tests
  • lowering unit test asserting temps whose address escapes via ADR are pinned and later statements get their own slot
  • lit end-to-end suite, including a new test asserting an unassigned aggregate return reads as an empty string

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

Build Artifacts

🐧 Linux

Artifact Link Size
deb-x86_64 Download 38.4 MB
schema Download 0.0 MB
stdlib Download 32.3 MB
plc-x86_64 Download 43.4 MB
deb-aarch64 Download 30.8 MB
plc-aarch64 Download 43.3 MB

From workflow run

🪟 Windows

Artifact Link Size
stdlib.lib Download 4.0 MB
stdlib.dll Download 0.1 MB
plc.exe Download 38.2 MB

From workflow run

@riederm

riederm commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

ui .... bit unusual to see such an optimization in the frontend, but the complexity is surprisingly low. On the other hand, aren't both strategies (lifetimes and tmp-slots) try to solve the exact same problem? do the lifetimes still have any effect if you optimize already in the compiler-frontend?

@riederm

riederm commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

ui .... bit unusual to see such an optimization in the frontend, but the complexity is surprisingly low. On the other hand, aren't both strategies (lifetimes and tmp-slots) try to solve the exact same problem? do the lifetimes still have any effect if you optimize already in the compiler-frontend?

... I just re-read my comment and I have to add - the complexity is lower than I expected, but it is still very unusual complexity in the compiler frontend 😆

@ghaith

ghaith commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

ui .... bit unusual to see such an optimization in the frontend, but the complexity is surprisingly low. On the other hand, aren't both strategies (lifetimes and tmp-slots) try to solve the exact same problem? do the lifetimes still have any effect if you optimize already in the compiler-frontend?

So i wanted to only do the lifetimes, but the problem is that the builder we are shipping in the IDE builds with -Onone and hence the lifetime does not take effect

Every call to an aggregate-returning function allocates a caller-side
temporary sized to the declared return type (STRING[2048] for the
stdlib string functions). These allocas carried no lifetime
information, so LLVM kept one dead slot per call site alive for the
whole function: string-heavy POU bodies easily exceed 100 KB of frame
and overflow constrained runtime task stacks.

Bracket each lowered statement's temporaries with
llvm.lifetime.start/end so LLVM's stack coloring can overlap slots of
consecutive statements; a string-heavy test body shrinks from ~111 KB
to ~10 KB of frame at the default optimization level. Unoptimized
builds are unchanged: stack coloring is a machine pass that only runs
when the backend optimizes.

Temps whose address outlives their statement are pinned instead
(Allocation::statement_scoped = false) and get no markers: ADR/REF
arguments, REF= right-hand sides, and interface fat-pointer captures
(the polymorphism lowering wraps those in ADR before this pass runs).
Loop bookkeeping temporaries keep their function-long storage for the
same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ghaith
ghaith force-pushed the fix/lowered-temp-stack-usage branch from 9d1d102 to e1a8cda Compare July 20, 2026 09:03
@ghaith ghaith changed the title Fix/lowered temp stack usage fix: emit lifetime markers for lowered call temporaries Jul 20, 2026
@ghaith
ghaith added this pull request to the merge queue Jul 28, 2026
Merged via the queue into master with commit f2efcb0 Jul 28, 2026
22 checks passed
@ghaith
ghaith deleted the fix/lowered-temp-stack-usage branch July 28, 2026 06:22
ghaith added a commit that referenced this pull request Jul 29, 2026
Backport of #1803 to `release/1.0.x`.

## Problem

Calling a function that returns an aggregate (string, struct) allocates
a caller-side temporary sized to the *declared* return type —
`STRING[2048]` for every stdlib string function — with no lifetime
information attached, so LLVM keeps every slot alive for the whole
function. A body with ~50 string operations already needs >100 KB of
frame and overflows small task stacks at function entry.

## Fix

Emit `llvm.lifetime.start`/`end` around each lowered statement's call
temporaries so LLVM's stack coloring can overlap slots whose lifetimes
don't intersect. Temporaries whose address escapes their statement
(`ADR`/`REF` arguments, `REF=` right-hand sides, interface fat-pointer
captures) are pinned and keep their function-long slot; loop bookkeeping
temporaries are likewise unaffected.

The markers only take effect when the LLVM backend optimizes: stack
coloring is a machine pass that does not run at `-Onone`. A companion
change for this lane enabling that pass for `-Onone` builds follows
separately.

Differences from the master commit are conflict resolution only: this
lane's `AggregateTypeLowerer` predates the #1794 rewrite, so the
address-context tracking is applied to the older structure.

## Testing

Full unit suite, snapshot tests (no drift against this lane's baseline)
and the lit end-to-end suite pass on the branch; includes the new
marker-placement, pinning, and unassigned-return tests from #1803.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Jul 29, 2026
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.

2 participants