diff --git a/cardano-constitution/test/Cardano/Constitution/Validator/Data/GoldenTests/unsorted.golden.uplc b/cardano-constitution/test/Cardano/Constitution/Validator/Data/GoldenTests/unsorted.golden.uplc index bef92027c38..b761f20018e 100644 --- a/cardano-constitution/test/Cardano/Constitution/Validator/Data/GoldenTests/unsorted.golden.uplc +++ b/cardano-constitution/test/Cardano/Constitution/Validator/Data/GoldenTests/unsorted.golden.uplc @@ -741,19 +741,18 @@ program (constr 3 [ (constr 1 [ cse - , cse ]) ])) + , (constr 1 + [ (constr 0 + [ (constr 0 + [ ]) + , (constr 1 + [ cse + , cse ]) ]) + , (constr 0 + [ ]) ]) ]) ])) (constr 1 - [ (constr 0 - [ (constr 0 - [ ]) - , (constr 1 - [ cse - , (constr 1 - [ (unsafeRatio - 9 - 10) - , (constr 0 - [ ]) ]) ]) ]) + [ (cse + 4) , (constr 0 [ ]) ])) (constr 3 @@ -776,33 +775,34 @@ program (constr 3 [ (constr 1 [ cse - , (constr 1 - [ (constr 0 - [ (constr 0 - [ ]) - , (constr 1 - [ cse - , cse ]) ]) - , (constr 0 - [ ]) ]) ]) ])) - (constr 1 - [ (cse - 4) - , (constr 0 - [ ]) ])) - (constr 0 - [ (constr 1 - [ ]) - , (constr 1 - [ cse - , (constr 1 - [ (unsafeRatio - 51 - 100) - , (constr 0 - [ ]) ]) ]) ])) - (cse - 2)) ]) + , cse ]) ])) + (constr 0 + [ (constr 1 + [ ]) + , (constr 1 + [ cse + , (constr 1 + [ (unsafeRatio + 51 + 100) + , (constr 0 + [ ]) ]) ]) ])) + (cse + 2)) + (constr 1 + [ (constr 0 + [ (constr 0 + [ ]) + , (constr 1 + [ cse + , (constr 1 + [ (unsafeRatio + 9 + 10) + , (constr 0 + [ ]) ]) ]) ]) + , (constr 0 + [ ]) ])) ]) (constr 0 [ (constr 1 [ ]) diff --git a/cardano-constitution/test/Cardano/Constitution/Validator/GoldenTests/unsorted.golden.uplc b/cardano-constitution/test/Cardano/Constitution/Validator/GoldenTests/unsorted.golden.uplc index 900636e10dd..e1b5e4930fa 100644 --- a/cardano-constitution/test/Cardano/Constitution/Validator/GoldenTests/unsorted.golden.uplc +++ b/cardano-constitution/test/Cardano/Constitution/Validator/GoldenTests/unsorted.golden.uplc @@ -742,62 +742,62 @@ program [ ]) , (constr 1 [ cse - , cse ]) ]) + , (constr 1 + [ (unsafeRatio + 4 + 5) + , (constr 0 + [ ]) ]) ]) ]) , (constr 0 [ ]) ]) ]) ])) - (constr 1 - [ (cse - 4) - , (constr 0 - [ ]) ])) - (constr 3 - [ (constr 1 - [ cse - , cse ]) ])) - (constr 1 - [ (constr 0 - [ (constr 0 - [ ]) - , (constr 1 - [ cse - , (constr 1 - [ (unsafeRatio - 9 - 10) - , (constr 0 - [ ]) ]) ]) ]) - , (constr 0 - [ ]) ])) - (constr 3 + (constr 3 + [ (constr 1 + [ cse + , (constr 1 + [ (constr 0 + [ (constr 0 + [ ]) + , (constr 1 + [ cse + , cse ]) ]) + , (constr 0 + [ ]) ]) ]) ])) + (constr 1 + [ (cse + 4) + , (constr 0 + [ ]) ])) + (constr 3 + [ (constr 1 + [ cse + , cse ]) ])) + (constr 0 [ (constr 1 + [ ]) + , (constr 1 [ cse , (constr 1 - [ (constr 0 - [ (constr 0 - [ ]) - , (constr 1 - [ cse - , (constr 1 - [ (unsafeRatio - 4 - 5) - , (constr 0 - [ ]) ]) ]) ]) + [ (unsafeRatio + 51 + 100) , (constr 0 [ ]) ]) ]) ])) - (constr 0 - [ (constr 1 - [ ]) - , (constr 1 - [ cse - , (constr 1 - [ (unsafeRatio - 51 - 100) - , (constr 0 - [ ]) ]) ]) ])) - (cse - 2)) ]) + (cse + 2)) + (constr 1 + [ (constr 0 + [ (constr 0 + [ ]) + , (constr 1 + [ cse + , (constr 1 + [ (unsafeRatio + 9 + 10) + , (constr 0 + [ ]) ]) ]) ]) + , (constr 0 + [ ]) ])) ]) (constr 0 [ (constr 1 [ ]) diff --git a/plutus-benchmark/bitwise/test/9.6/Ed25519.golden.uplc b/plutus-benchmark/bitwise/test/9.6/Ed25519.golden.uplc index 3a4a8e84554..78281e16f75 100644 --- a/plutus-benchmark/bitwise/test/9.6/Ed25519.golden.uplc +++ b/plutus-benchmark/bitwise/test/9.6/Ed25519.golden.uplc @@ -966,12 +966,12 @@ (case ds [ (\w - cont -> + rest -> w) ])) (case ds [ (\w - rest -> + cont -> w) ])) ]) (next ipv)) ]) diff --git a/plutus-tx-plugin/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md b/plutus-tx-plugin/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md new file mode 100644 index 00000000000..168ec7094cb --- /dev/null +++ b/plutus-tx-plugin/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md @@ -0,0 +1,4 @@ +### Added + +- Compilation support for the `multiIndexArray` builtin + ([CIP-0156](https://cips.cardano.org/cip/CIP-0156)). diff --git a/plutus-tx-plugin/src/PlutusTx/Compiler/Builtins.hs b/plutus-tx-plugin/src/PlutusTx/Compiler/Builtins.hs index 7ce5223eb01..9a456a3708c 100644 --- a/plutus-tx-plugin/src/PlutusTx/Compiler/Builtins.hs +++ b/plutus-tx-plugin/src/PlutusTx/Compiler/Builtins.hs @@ -234,6 +234,7 @@ builtinNames = , 'Builtins.lengthOfArray , 'Builtins.listToArray , 'Builtins.indexArray + , 'Builtins.multiIndexArray , ''Builtins.BuiltinData , 'Builtins.chooseData , 'Builtins.equalsData @@ -753,8 +754,7 @@ defineBuiltinTerms = do PLC.LengthOfArray -> defineBuiltinInl 'Builtins.lengthOfArray PLC.ListToArray -> defineBuiltinInl 'Builtins.listToArray PLC.IndexArray -> defineBuiltinInl 'Builtins.indexArray - -- Not exposed to Plinth yet: core-language only for now (CIP-0156). - PLC.MultiIndexArray -> pure () + PLC.MultiIndexArray -> defineBuiltinInl 'Builtins.multiIndexArray -- Data PLC.ChooseData -> defineBuiltinInl 'Builtins.chooseData PLC.EqualsData -> defineBuiltinInl 'Builtins.equalsData diff --git a/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.eval b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.eval new file mode 100644 index 00000000000..3eed8238179 --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.eval @@ -0,0 +1 @@ +[I 3, I 1, I 1, I 2] \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.pir b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.pir new file mode 100644 index 00000000000..2535175de8d --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.pir @@ -0,0 +1,27 @@ +(let + !multiIndexArray : all a. list integer -> array a -> list a + = multiIndexArray + in + multiIndexArray {data}) + [2,0,0,1] + (let + !unitval : unit = () + in + let + !mkNilData : unit -> list data = mkNilData + in + let + !mkI : integer -> data = iData + in + let + !mkCons : all a. a -> list a -> list a = mkCons + in + let + !listToArray : all a. list a -> array a = listToArray + in + listToArray + {data} + (mkCons + {data} + (mkI 1) + (mkCons {data} (mkI 2) (mkCons {data} (mkI 3) (mkNilData unitval))))) \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.uplc b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.uplc new file mode 100644 index 00000000000..d46c34f4382 --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.12/compiledMultiIndexArray.golden.uplc @@ -0,0 +1,21 @@ +(program + 1.1.0 + ((\multiIndexArray -> force multiIndexArray) + multiIndexArray + [2,0,0,1] + ((\unitval -> + (\mkNilData -> + (\mkI -> + (\mkCons -> + (\listToArray -> + force listToArray + (force mkCons + (mkI 1) + (force mkCons + (mkI 2) + (force mkCons (mkI 3) (mkNilData unitval))))) + listToArray) + mkCons) + iData) + mkNilData) + ()))) \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.eval b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.eval new file mode 100644 index 00000000000..3eed8238179 --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.eval @@ -0,0 +1 @@ +[I 3, I 1, I 1, I 2] \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.pir b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.pir new file mode 100644 index 00000000000..2535175de8d --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.pir @@ -0,0 +1,27 @@ +(let + !multiIndexArray : all a. list integer -> array a -> list a + = multiIndexArray + in + multiIndexArray {data}) + [2,0,0,1] + (let + !unitval : unit = () + in + let + !mkNilData : unit -> list data = mkNilData + in + let + !mkI : integer -> data = iData + in + let + !mkCons : all a. a -> list a -> list a = mkCons + in + let + !listToArray : all a. list a -> array a = listToArray + in + listToArray + {data} + (mkCons + {data} + (mkI 1) + (mkCons {data} (mkI 2) (mkCons {data} (mkI 3) (mkNilData unitval))))) \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.uplc b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.uplc new file mode 100644 index 00000000000..d46c34f4382 --- /dev/null +++ b/plutus-tx-plugin/test/Array/9.6/compiledMultiIndexArray.golden.uplc @@ -0,0 +1,21 @@ +(program + 1.1.0 + ((\multiIndexArray -> force multiIndexArray) + multiIndexArray + [2,0,0,1] + ((\unitval -> + (\mkNilData -> + (\mkI -> + (\mkCons -> + (\listToArray -> + force listToArray + (force mkCons + (mkI 1) + (force mkCons + (mkI 2) + (force mkCons (mkI 3) (mkNilData unitval))))) + listToArray) + mkCons) + iData) + mkNilData) + ()))) \ No newline at end of file diff --git a/plutus-tx-plugin/test/Array/Spec.hs b/plutus-tx-plugin/test/Array/Spec.hs index 4230c07cc25..25b71b8d579 100644 --- a/plutus-tx-plugin/test/Array/Spec.hs +++ b/plutus-tx-plugin/test/Array/Spec.hs @@ -15,6 +15,7 @@ module Array.Spec where import PlutusCore.Test (goldenUEval) import PlutusTx +import PlutusTx.Builtins (toBuiltin) import PlutusTx.Builtins.Internal import PlutusTx.Test (goldenPirReadable, goldenUPlcReadable) import Test.Tasty.Extras @@ -33,6 +34,9 @@ smokeTests = , goldenPirReadable "compiledIndexArray" compiledIndexArray , goldenUPlcReadable "compiledIndexArray" compiledIndexArray , goldenUEval "compiledIndexArray" [compiledIndexArray] + , goldenPirReadable "compiledMultiIndexArray" compiledMultiIndexArray + , goldenUPlcReadable "compiledMultiIndexArray" compiledMultiIndexArray + , goldenUEval "compiledMultiIndexArray" [compiledMultiIndexArray] ] ] @@ -63,3 +67,9 @@ compiledIndexArray = $$(compile [||indexArray||]) `unsafeApplyCode` compiledListToArray `unsafeApplyCode` liftCodeDef 2 + +compiledMultiIndexArray :: CompiledCode (BuiltinList BuiltinData) +compiledMultiIndexArray = + $$(compile [||multiIndexArray||]) + `unsafeApplyCode` liftCodeDef (toBuiltin ([2, 0, 0, 1] :: [Integer])) + `unsafeApplyCode` compiledListToArray diff --git a/plutus-tx/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md b/plutus-tx/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md new file mode 100644 index 00000000000..62f593ff346 --- /dev/null +++ b/plutus-tx/changelog.d/20260715_113448_yuriy.lazaryev_issue_2265_multi_index_array_plinth.md @@ -0,0 +1,5 @@ +### Added + +- `PlutusTx.Builtins.multiIndexArray`: the `multiIndexArray` builtin from + [CIP-0156](https://cips.cardano.org/cip/CIP-0156), returning the array elements at the given + indices and failing on any out-of-bounds index. diff --git a/plutus-tx/plutus-tx.cabal b/plutus-tx/plutus-tx.cabal index c581391ebcf..49204031e69 100644 --- a/plutus-tx/plutus-tx.cabal +++ b/plutus-tx/plutus-tx.cabal @@ -216,6 +216,7 @@ test-suite plutus-tx-test main-is: Spec.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N other-modules: + Array.Spec Blueprint.Definition.Fixture Blueprint.Definition.Spec Blueprint.Spec diff --git a/plutus-tx/src/PlutusTx/Builtins.hs b/plutus-tx/src/PlutusTx/Builtins.hs index 5750210b7b4..9f03dd11fe7 100644 --- a/plutus-tx/src/PlutusTx/Builtins.hs +++ b/plutus-tx/src/PlutusTx/Builtins.hs @@ -100,6 +100,7 @@ module PlutusTx.Builtins , sopListToArray , BI.lengthOfArray , BI.indexArray + , BI.multiIndexArray -- * Tracing , trace diff --git a/plutus-tx/src/PlutusTx/Builtins/Internal.hs b/plutus-tx/src/PlutusTx/Builtins/Internal.hs index b49a7397791..1518d383a8e 100644 --- a/plutus-tx/src/PlutusTx/Builtins/Internal.hs +++ b/plutus-tx/src/PlutusTx/Builtins/Internal.hs @@ -688,6 +688,20 @@ indexArray (BuiltinArray v) i | otherwise = Haskell.error "array index out of bounds" {-# OPAQUE indexArray #-} +{-| Returns the elements at the given indices, in index-list order with duplicates preserved. + Fails if any index is not in the range @[0..j)@, where @j@ is the length of the array. -} +multiIndexArray :: BuiltinList BuiltinInteger -> BuiltinArray a -> BuiltinList a +multiIndexArray (BuiltinList is) (BuiltinArray v) = + case traverse lookupIndex is of + Just els -> BuiltinList els + Nothing -> Haskell.error "array index out of bounds" + where + len = toInteger (Vector.length v) + lookupIndex i + | 0 <= i && i < len = Just (Vector.unsafeIndex v (fromInteger i)) + | otherwise = Nothing +{-# OPAQUE multiIndexArray #-} + {- BLS12_381 -} diff --git a/plutus-tx/test/Array/Spec.hs b/plutus-tx/test/Array/Spec.hs new file mode 100644 index 00000000000..86c8fa0dfaf --- /dev/null +++ b/plutus-tx/test/Array/Spec.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE TypeApplications #-} + +module Array.Spec (arrayTests) where + +import Control.Exception (ErrorCall) +import Data.Either (isLeft) +import PlutusCore.Test (pureTry) +import PlutusTx.Builtins.Internal qualified as BI + +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (Assertion, assertBool, testCase, (@?=)) +import Prelude + +-- The wrapper must fail exactly where the on-chain builtin fails: any index outside +-- @[0..length)@, including one exceeding @maxBound :: Int@, is an error rather than +-- a wrap-around lookup. +arrayTests :: TestTree +arrayTests = + testGroup + "Array" + [ testGroup + "multiIndexArray" + [ testCase "in-order with duplicates" $ + unList (BI.multiIndexArray (BI.BuiltinList [2, 0, 0, 1]) arr) @?= [30, 10, 10, 20] + , testCase "index exceeding maxBound::Int fails" $ + throwsErrorCall (BI.multiIndexArray (BI.BuiltinList [2 ^ (64 :: Int) + 2]) arr) + , testCase "out-of-bounds index fails even if the result is not fully demanded" $ + throwsErrorCall + ( case unList (BI.multiIndexArray (BI.BuiltinList [0, 3]) arr) of + x : _ -> x + [] -> 0 + ) + ] + ] + where + arr :: BI.BuiltinArray BI.BuiltinInteger + arr = BI.listToArray (BI.BuiltinList [10, 20, 30]) + + unList :: BI.BuiltinList a -> [a] + unList (BI.BuiltinList xs) = xs + + throwsErrorCall :: a -> Assertion + throwsErrorCall x = + assertBool "expected the evaluation to fail" (isLeft (pureTry @ErrorCall x)) diff --git a/plutus-tx/test/Spec.hs b/plutus-tx/test/Spec.hs index ce6a29b81b3..379bca11364 100644 --- a/plutus-tx/test/Spec.hs +++ b/plutus-tx/test/Spec.hs @@ -5,6 +5,7 @@ module Main (main) where +import Array.Spec (arrayTests) import Blueprint.Definition.Spec qualified import Bool.Spec (boolTests) import Builtins.Spec (builtinsTests) @@ -41,7 +42,8 @@ tests :: TestTree tests = testGroup "plutus-tx" - [ serdeTests + [ arrayTests + , serdeTests , sqrtTests , ratioTests , bytestringTests