Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.d/20260704_120000_unisay_table_accessor_guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Fixed

- The Lua optimizer rule that folds a field access into a table literal
(`{ foo = 1 }.foo` to `1`) now declines when the constructor has a
string-keyed row or a repeated field name, instead of silently folding to
`nil` or to the wrong (first) value. Neither shape is emitted by the
current codegen, so this closes a latent miscompile rather than a live one
(#140).
39 changes: 30 additions & 9 deletions lib/Language/PureScript/Backend/Lua/Optimizer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Language.PureScript.Backend.Lua.Traversal
, everywhereStatM
)
import Language.PureScript.Backend.Lua.Types
( Chunk
( Annotated
, Chunk
, Exp
, ExpF (..)
, Statement
Expand Down Expand Up @@ -69,14 +70,34 @@ removeScopeWhenInsideEmptyFunction = \case
Function outerArgs body
e → e

-- | Rewrites '{ foo = 1, bar = 2 }.foo' to '1'
{- | Rewrites '{ foo = 1, bar = 2 }.foo' to '1'.

Only fires when the constructor is unambiguous: every row is a name-value
row and no field name repeats. A 'TableRowKV' row could carry a string key
equal to the accessed field (e.g. @["foo"] = …@) that this name-keyed lookup
cannot see, and on a repeated name Lua's constructor keeps the last
assignment while a first-match lookup returns the earliest; in either case
the fold could pick the wrong value, so the rule declines. See issue #140.
-}
reduceTableDefinitionAccessor ∷ RewriteRule
reduceTableDefinitionAccessor = \case
Var (Ann (VarField (Ann (TableCtor rows)) accessedField)) →
fromMaybe Nil $
listToMaybe
[ fieldValue
| (_ann, TableRowNV tableField (Ann fieldValue)) ← rows
, tableField == accessedField
]
original@(Var (Ann (VarField (Ann (TableCtor rows)) accessedField)))
| all isNameValue rows
, not (hasDuplicateNames rows) →
fromMaybe Nil $
listToMaybe
[ fieldValue
| (_ann, TableRowNV tableField (Ann fieldValue)) ← rows
, tableField == accessedField
]
| otherwise → original
e → e
where
isNameValue ∷ Annotated () TableRowF → Bool
isNameValue (_ann, row) = case row of
TableRowNV {} → True
TableRowKV {} → False
hasDuplicateNames ∷ [Annotated () TableRowF] → Bool
hasDuplicateNames rows =
let names = [n | (_ann, TableRowNV n _) ← rows]
in length names /= length (ordNub names)
40 changes: 39 additions & 1 deletion test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ module Language.PureScript.Backend.Lua.Optimizer.Spec where

import Language.PureScript.Backend.Lua.Name (name)
import Language.PureScript.Backend.Lua.Optimizer
( removeScopeWhenInsideEmptyFunction
( reduceTableDefinitionAccessor
, removeScopeWhenInsideEmptyFunction
, rewriteExpWithRule
)
import Language.PureScript.Backend.Lua.Types (ParamF (..))
Expand Down Expand Up @@ -37,3 +38,40 @@ spec = describe "Lua AST Optimizer" do
]
assertEqual (toString $ pShow original) expected $
rewriteExpWithRule removeScopeWhenInsideEmptyFunction original

describe "reduceTableDefinitionAccessor" do
it "folds a field access into an unambiguous name-value definition" do
let original ∷ Lua.Exp =
Lua.varField
( Lua.table
[ Lua.tableRowNV [name|foo|] (Lua.Integer 1)
, Lua.tableRowNV [name|bar|] (Lua.Integer 2)
]
)
[name|foo|]
assertEqual (toString $ pShow original) (Lua.Integer 1) $
rewriteExpWithRule reduceTableDefinitionAccessor original

it "declines when a key-value row could shadow the accessed field" do
-- The accessed field is present only as a string-keyed row, which the
-- name lookup cannot see, so folding to Nil would drop the real value.
let original ∷ Lua.Exp =
Lua.varField
(Lua.table [Lua.tableRowKV (Lua.String "foo") (Lua.Integer 1)])
[name|foo|]
assertEqual (toString $ pShow original) original $
rewriteExpWithRule reduceTableDefinitionAccessor original

it "declines on duplicate field names, where Lua keeps the last" do
-- Lua's table constructor keeps the last assignment (2); a first-match
-- fold would wrongly return the first (1).
let original ∷ Lua.Exp =
Lua.varField
( Lua.table
[ Lua.tableRowNV [name|foo|] (Lua.Integer 1)
, Lua.tableRowNV [name|foo|] (Lua.Integer 2)
]
)
[name|foo|]
assertEqual (toString $ pShow original) original $
rewriteExpWithRule reduceTableDefinitionAccessor original
Loading