Skip to content

Make the runtime-lazy factory a first-class IR reference #212

Description

@Unisay

Self-recursive (lazy) bindings compile to a call into a runtime helper, the "runtime lazy factory". The reference to that helper travels through the IR as a bare local name, the literal string PSLUA_runtime_lazy. Nothing in the IR marks it as special: it looks exactly like a local variable a user could have written. So the same string ends up hard-wired independently at five sites that all have to agree, and several passes recognize the factory by comparing against that string rather than by structure.

The coupling is already documented in Note [The PSLUA_runtime_lazy coupling] in Language.PureScript.Names. The five sites:

  • Names.runtimeLazyName: the ident the laziness transform emits (InternalIdent RuntimeLazyFactory, whose runIdent is this string), so it surfaces in the IR as Local (Name "PSLUA_runtime_lazy").
  • Backend.Lua.Fixture.runtimeLazyName: the name of the hand-written Lua fixture, rebuilt independently as psluaName [name|runtime_lazy|]. It has to mangle to the same string, or the emitted call targets a Lua local that does not exist.
  • Backend.IR.Query.usesRuntimeLazy: scans the linked UberModule for a free Local (Name runtimeLazyName) to decide whether the fixture is referenced.
  • Backend and Backend.Lua.fromUberModule: emit the fixture only when it is both needed and used.
  • Backend.IR.Linter.unboundLocals: seeds its scope with runtimeLazyName so the free reference does not trip the well-scopedness check. This is the one deliberate exemption from that invariant.

Rename either half of the string and the two sides stop matching: the fixture is judged unused and dropped, or the generated code calls a binding that was never emitted. Either way laziness miscompiles silently, with no type error and nothing from the linter. The linter exemption is the sharper cost. unboundLocals is otherwise a total well-scopedness check, and this one magic name pokes a permanent hole in it.

Proposed change

Represent the factory reference first-class in the IR instead of as a magic local name. Two shapes work:

  • a dedicated RawExp constructor for the factory reference, or
  • a single smart constructor plus a recognizer (or a pattern synonym) that every consumer routes through.

The goal is the same either way. The laziness transform emits the structured node, usesRuntimeLazy and the linter exemption match on it by structure rather than by string equality, and the literal PSLUA_runtime_lazy collapses to one definition site. Only the Lua fixture still needs the concrete Lua identifier, because Lua names are strings by nature, but the IR side stops carrying the string at all, so there is no longer a string for the two halves to disagree on. The linter exemption then becomes structural recognition of a known node instead of a name seeded into scope.

RuntimeLazyFactory is already a constructor of InternalIdentData, so the factory is structured on the CoreFn / Ident side. What this issue asks for is preserving that structure across the CoreFn to IR boundary instead of flattening it to a string through runIdent.

Scope

This is the small, self-contained half of a larger observation about laziness lowering. The bigger idea, that the whole Lazy_* / runtime_lazy pair generation should move out of the CoreFn to IR boundary and into a late IR pass, is deliberately left for a separate issue: it carries real design questions (interaction with DCE and the inliner) and may not pay off, whereas this cleanup is low-risk and stands on its own. If that redesign does happen later, the factory reference wants to be first-class anyway, so this is a useful first step rather than throwaway work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: irIR / optimizer / DCE / inlinerchoreMaintenance / infrastructure

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions