Skip to content

CPR-style worker/wrapper on results: return constructed products as Lua multiple values #206

Description

@Unisay

Problem

A function that always returns a constructed product allocates a table per call even when every caller immediately deconstructs the result. State-shaped code pays this on every bind (a fresh Tuple per step), uncons/splitAt-style helpers pay it per element. The allocation exists only to carry two or three values across a return boundary.

Approach

The result-side twin of the #24 worker/wrapper split (GHC calls it CPR, Constructed Product Result), with Lua's native multiple return values as the unboxed-tuple representation:

-- worker returns components, no table
local step_w = function(s) return a, s2 end
-- wrapper reboxes for callers that consume the product as a value
local step = function(s)
  local a, s2 = step_w(s)
  return { ["$ctor"] = "...Tuple", value0 = a, value1 = s2 }
end

At a call site that immediately deconstructs the result, the inlined wrapper's rebox meets the #177 fold and the product never materializes. The termination side condition GHC's CPR carries (botCpr, unlifted results must not hide bottom) vanishes here: evaluation is strict, so the worker either reaches its return or the program has already diverged, and returning multiple values changes nothing about that.

What it takes, and why this is deliberately parked rather than next in line:

  • an IR representation for multi-value results (functions currently return exactly one value);
  • consumption-aware rewriting of call sites: only deconstructing sites bypass the wrapper, a result flowing away as a first-class value keeps going through it;
  • interaction with the Program transformation to uncurry functions for which all applications are fully saturated. #24 split: workers are the natural place to change the return convention, so this is a second story on top of IR/Uncurry.hs.

Amulet ships exactly this shape ("unboxed tuples as multiple simultaneous Lua values"), so there is a working precedent on the same target.

Prerequisites / Relations

Builds on the #24 machinery (landed in #202) and on #177 as the rebox canceller. Parked until #198, #180 and #186 have landed and the #172 counters show what share of allocations are immediately-deconstructed products; that number decides whether the plumbing above is worth it.

Verification / Measurement

An allocation counter on a State-shaped benchmark (one Tuple per bind step today) drops to near zero table allocations per step, while eval goldens stay byte-identical. #172 registers the end-to-end effect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inliner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions