@@ -27,7 +27,7 @@ import PlutusTx.Numeric
2727import PlutusTx.Prelude hiding (integerToByteString )
2828import PlutusTx.Show (toDigits )
2929import PlutusTx.TH (compile )
30- import PlutusTx.Test.Run.Code (evalResult , evaluateCompiledCode , evaluationResultMatchesHaskell )
30+ import PlutusTx.Test.Run.Code (evalResult , evaluateCompiledCode )
3131import PlutusTx.Traversable qualified as Tx
3232
3333import PlutusCore.Builtin qualified as PLC
@@ -319,16 +319,61 @@ test_valueOf =
319319 `unsafeApplyCode` liftCodeDef (unTokenName tn)
320320 in nonBuiltin === builtin
321321
322- {-| Check that running the compiled fused 'unionWith' on CEK produces the
323- same 'Value' as the host-Haskell implementation, for arbitrary pairs of
324- 'Value's. The combining function must come from 'PlutusTx.Prelude' so
325- that Plinth can inline it into the compiled UPLC. -}
322+ {-| The 'unionWith' @(+)@ under test. Signature matches 'compiledBuiltinUnion' so the property
323+ can pit them against each other. -}
324+ compiledUnionWith :: CompiledCode (BI. BuiltinData -> BI. BuiltinData -> BI. BuiltinData )
325+ compiledUnionWith = plinthc \ bd1 bd2 ->
326+ Tx. toBuiltinData (unionWith (+) (Tx. unsafeFromBuiltinData bd1) (Tx. unsafeFromBuiltinData bd2))
327+
328+ {-| Independent oracle: the builtin union path. Shares no source with 'compiledUnionWith', so a
329+ bug in one cannot hide behind the same bug in the other. -}
330+ compiledBuiltinUnion :: CompiledCode (BI. BuiltinData -> BI. BuiltinData -> BI. BuiltinData )
331+ compiledBuiltinUnion = plinthc \ bd1 bd2 ->
332+ B. mkValue (B. unionValue (B. unsafeDataAsValue bd1) (B. unsafeDataAsValue bd2))
333+
334+ -- | Evaluate a compiled union on CEK and decode its result.
335+ runUnionCode
336+ :: CompiledCode (BI. BuiltinData -> BI. BuiltinData -> BI. BuiltinData )
337+ -> Value
338+ -> Value
339+ -> Value
340+ runUnionCode code value1 value2 =
341+ Tx. unsafeFromBuiltinData
342+ . BI. dataToBuiltinData
343+ . either Haskell. throw id
344+ $ errOrRes
345+ >>= PLC. readKnownSelf
346+ where
347+ prog =
348+ code
349+ `unsafeApplyCode` liftCodeDef (Tx. toBuiltinData value1)
350+ `unsafeApplyCode` liftCodeDef (Tx. toBuiltinData value2)
351+ (errOrRes, _cost) =
352+ PLC. runCekNoEmit PLC. defaultCekParametersForTesting PLC. counting
353+ . PLC. runQuote
354+ . PLC. unDeBruijnTermWith (Haskell. error " Free variable" )
355+ . PLC. _progTerm
356+ $ getPlc prog
357+
358+ -- | 'unionWith' @(+)@ must agree with the builtin union path on CEK.
326359test_unionWith :: TestTree
327360test_unionWith =
328- testProperty " unionWith on CEK matches host Haskell" \ value1 value2 ->
329- let compiled =
330- $$ (compile [||\ v1 v2 -> unionWith (+) v1 v2|| ])
331- `unsafeApplyCode` liftCodeDef value1
332- `unsafeApplyCode` liftCodeDef value2
333- expected = unionWith (+) value1 value2
334- in evaluationResultMatchesHaskell compiled (===) expected
361+ testProperty " non-builtin unionWith matches builtin unionValue on CEK" \ rawValue1 rawValue2 ->
362+ let v1 = normalise rawValue1
363+ v2 = normalise rawValue2
364+ -- Compare semantically: key order and zero-sum entries differ between the paths but
365+ -- carry no meaning, so canonicalise before '==='.
366+ canon code = normaliseLists . valueToLists $ runUnionCode code v1 v2
367+ in canon compiledUnionWith === canon compiledBuiltinUnion
368+
369+ {-| Restrict an arbitrary 'Value' to the well-formed domain 'unsafeDataAsValue' accepts: the
370+ builtin errors on unsorted keys, zero quantities, or empty token maps. -}
371+ normaliseLists
372+ :: [(CurrencySymbol , [(TokenName , Integer )])] -> [(CurrencySymbol , [(TokenName , Integer )])]
373+ normaliseLists =
374+ Haskell. sortOn fst
375+ . Haskell. filter (Haskell. not . Haskell. null . snd )
376+ . Haskell. map (Haskell. fmap (Haskell. sortOn fst . Haskell. filter ((Haskell. /= 0 ) . snd )))
377+
378+ normalise :: Value -> Value
379+ normalise = listsToValue . normaliseLists . valueToLists
0 commit comments