Skip to content

Lift the pure subset of foreign values into the IR: primop nodes and an allowlist-driven lifter #178

Description

@Unisay

Problem

Polymorphic hot code bottoms out in opaque curried foreigns, and the optimizer cannot finish the specialization it starts: greaterThanOrEq(ordInt)(a)(b) stays a dictionary chain through compare and Ordering tables instead of a >= b — measured 42x slower (PUC 5.1, 5e6 comparisons); arithmetic through inlined foreign lambdas is 8.3x on the Fibonacci golden. Existing rules (beta, inlining, #177) stop where foreign bodies begin, because to the IR those bodies are text.

The original plan was a hand-written registry mapping qualified names to IR semantics (the purs-backend-es foreignSemantics approach for JS). The own-parser decision (#173) enables something stronger for Lua: derive the semantics from the actual fork source, so registry drift against the package set is impossible by construction. Lua's tiny grammar is what makes lifting realistic where it is not for JS.

Approach

  1. Primop nodes in the IR, defined as the corresponding Lua operators so lowering is the identity and lifting is semantics-preserving by construction: binary PrimAdd/Sub/Mul/Div/Mod/Concat, comparisons PrimLt/Le/Gt/Ge, logic PrimAnd/Or/Not. Equality already exists as the Eq node; the lifter maps == onto it. The correctness burden falls only on rules over primops: constant folding of literals must follow Lua 5.1 semantics (double arithmetic), consistent with the existing constantFolding.
  2. A lifter at mergeForeigns time: for each foreign export on the allowlist, take its parsed Lua AST (Replace the lexical foreign splitter with a real Lua 5.1 parser producing the Lua AST #173) and translate the pure return-tree subset — nested function literals to Abs, if/return trees to IfThenElse, operators to primops, literals to literals, calls only to a whitelist of pure stdlib functions (math.floor, ...). Loops, mutation, varargs do not lift; the export stays an opaque ForeignImport (correct for e.g. foldlArray).
  3. The allowlist is a hard contract: an allowlisted export that fails to lift (a fork update changed the implementation shape) is a compile error, not a silent performance cliff.
  4. No new optimization rules needed downstream: existing beta reduction, inlining, Fold ReflectCtor and DataArgumentByIndex over known constructor applications (case-of-known-constructor) #177 folds, and constant folding finish greaterThanOrEq ordInt a b down to not (a < b).

Scope of this issue: primop nodes, the lifter, and the initial allowlist — the arithmetic/comparison/boolean/concat core of the prelude (intAdd/intMul/intSub, ordIntImpl/ordCharImpl, boolConj/boolDisj/boolNot, the refEq family, concatString) — exactly the measured 42x case. The *.Uncurried modules (which need an n-ary call node, #179) and the broader allowlist are follow-up work.

Prerequisites / Relations

Depends on #173 (the foreign parser supplies the Lua AST the lifter translates) and on #177 (case-of-known-constructor completes the cascade so lifted primops reduce all the way). Landing it in turn unblocks the dictionary-method inlining of #180. The n-ary call node the *.Uncurried follow-up needs lands in #179.

Verification / Measurement

Measured by #172: the dictionary-comparison microbenchmark (greaterThanOrEq(ordInt)(a)(b)) drops from 42x slower than native toward near 1x, and the Fibonacci arithmetic golden from 8.3x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inliner

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions