Skip to content

feat(optimizer): fold array length and indexing on literal arrays (#225) - #292

Merged
Unisay merged 1 commit into
mainfrom
issue-225/fold-literal-array-reads
Jul 24, 2026
Merged

feat(optimizer): fold array length and indexing on literal arrays (#225)#292
Unisay merged 1 commit into
mainfrom
issue-225/fold-literal-array-reads

Conversation

@Unisay

@Unisay Unisay commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #225.

A pattern match on a fixed-length array whose scrutinee is a manifest array literal is decidable at compile time, but the optimizer left it as a runtime length check plus element reads. This PR adds the three transformations the issue proposes, and the existing folds finish the job.

The rules

reduceArrayRead — the reduceObjectProp sibling for arrays: folds arrayLength over an in-place LiteralArray to its element count, and an in-range ArrayIndex to the element itself. The issue's piece (2) turned out simpler than proposed: the IR's ArrayIndex already carries a static Natural index, so no literal-index matching is needed. The length folds against the IR element count, which is exact — a LiteralArray codegens to a hole-free positional table — so the caveat about Lua's # over tables with holes never arises:

reduceArrayRead  Applicative m  RewriteRuleM m Ann
reduceArrayRead =
  pure . \case
    ArrayLength ann (LiteralArray _ elements) 
      Just $ LiteralInt ann (fromIntegral (length elements))
    ArrayIndex ann (LiteralArray _ elements) index 
      setAnn ann <$> elements !!? fromIntegral index
    _  Nothing

propagateKnownArrayThroughLet — the literal-array sibling of propagateKnownCtorThroughLet (#214). The match's scrutinee is let-bound and read several times (one length check plus one read per element), so it is never in place for reduceArrayRead to see. When the binder is read only through in-range array reads, each length read becomes the element count, each element read becomes a fresh element-binder bound once to the element (the field-binder discipline of #214, so an element read at several sites is never duplicated), and the binding is dropped. The rule declines on a whole-value read or an out-of-range index — Query.hasWholeValueArrayRead mirrors hasWholeValueRead including the out-of-range-counts-as-whole-value guard.

End to end

The motivating shape from the issue, in the Golden.ArrayPatternMatch golden — before:

local _ = Golden_ArrayPatternMatch_Test_logShow(Golden_ArrayPatternMatch_Test_firstTwo({
  [1] = 10,
  [2] = 20
}))()

after:

local _ = Effect_Console_log(Data_Show_showIntImpl(30))()

Every literal-array call site in that golden folds to its constant (30, -1, -1, 9); the eval/golden.txt oracle is untouched and still passes. The surviving M.Golden_ArrayPatternMatch_Test_firstTwo = … entries in the new golden.lua are the harness shape, not a leak: the golden pipeline links AsModule, so the now-uncalled helpers stay reachable as exports — the same residue every other golden with exported-but-unused-by-main helpers shows.

Tests

Test-first, per the issue: the unit specs were red before the rules landed. The in-place folds and their decline cases (out-of-range index, unknown-length array) are pinned at optimizedExpression level; the no-duplication discipline is pinned by an alphaEq check that a twice-read non-trivial element becomes one binder read twice. The full collapse runs through optimizedUberModuleChecked (mirroring the #214 end-to-end test), because the rewrite chain deliberately leaves the spent element-binders for DCE — that run also lints every pass boundary, so a GUC or scoping violation from folding through the Let would fail there. Two properties: the collapse across generated scalar elements, and well-scopedness when the index is fuzzed out of range:

prop "declines an out-of-range element read, staying well-scoped" do
  len  forAll (Gen.int (Range.linear 0 3))
  index  forAll (Gen.integral (Range.linear 0 5))
  let original =
        let1 (Name "v") (literalArray (replicate len (literalInt 1))) $
          application
            (application (refImported m (Name "pair")) (arrayIndex v 0))
            (arrayIndex v index)
  unboundLocals (optimizedExpression original) === []

cabal test all is green, and the randomized specs were stressed across several seeds.

@Unisay
Unisay merged commit 28c3d04 into main Jul 24, 2026
2 checks passed
@Unisay
Unisay deleted the issue-225/fold-literal-array-reads branch July 24, 2026 08:33
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.

Fold array-length and indexing on manifest array literals

1 participant