Skip to content

Commit 668d603

Browse files
authored
Fix: Use of fromString @BuiltinByteStringHex with inscrutable content (#6963)
* BuiltinByteStringHex: Unexpected error during compilation * fix: BuiltinByteString with inscrutable content. * Changelog entry * refactor: remove redundant parens
1 parent 9514385 commit 668d603

File tree

5 files changed

+113
-20
lines changed

5 files changed

+113
-20
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
### Fixed
2+
3+
- In some cases HEX-encoded string literals were not handled correctly,
4+
causing the "Use of fromString @ PlutusTx.Builtins.HasOpaque.BuiltinByteStringHex with inscrutable content" error.

plutus-tx-plugin/plutus-tx-plugin.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ test-suite plutus-tx-plugin-tests
132132
Budget.WithGHCOptimisations
133133
Budget.WithoutGHCOptimisations
134134
BuiltinList.Budget.Spec
135+
ByteStringLiterals.Lib
135136
ByteStringLiterals.Spec
136137
DataList.Budget.Spec
137138
Inline.Spec

plutus-tx-plugin/src/PlutusTx/Compiler/Expr.hs

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import Control.Monad.Reader (ask, asks, local)
7676
import Data.Array qualified as Array
7777
import Data.ByteString qualified as BS
7878
import Data.ByteString.Base16 qualified as Base16
79+
import Data.ByteString.Char8 qualified as BSC
7980
import Data.Generics.Uniplate.Data (transform, universeBi)
8081
import Data.List (elemIndex, isPrefixOf, isSuffixOf)
8182
import Data.Map qualified as Map
@@ -109,17 +110,34 @@ String literals are handled specially, see Note [String literals].
109110
-}
110111

111112
{- Note [unpackFoldrCString#]
112-
This function is introduced by rewrite rules, and usually eliminated by them in concert with `build`.
113+
This function is introduced by rewrite rules, and usually eliminated by them
114+
in concert with `build`.
113115
114-
However, since we often mark things as INLINABLE, we get pre-optimization Core where only the
115-
first transformation has fired. So we need to do something with the function.
116+
However, since we often mark things as INLINABLE, we get pre-optimization Core
117+
where only the first transformation has fired. So we need to do something with
118+
the function.
116119
117-
- We can't easily turn it into a normal fold expression, since we'd need to make a lambda and
118-
we're not in 'CoreM' so we can't make fresh names.
119-
- We can't easily translate it to a builtin, since we don't support higher-order functions.
120+
- We can't easily turn it into a normal fold expression, since we'd need to make
121+
a lambda and we're not in 'CoreM' so we can't make fresh names.
120122
121-
So we use a horrible hack and match on `build . unpackFoldrCString#` to "undo" the original rewrite
122-
rule.
123+
- We can't easily translate it to a builtin, since we don't support higher-order
124+
functions.
125+
126+
So we use a horrible hack and match on `build . unpackFoldrCString#` to "undo"
127+
the original rewrite rule.
128+
129+
Moreover, in some cases GHC generates such expressions where the first literal
130+
character is "un-consed" from its tail, for example:
131+
132+
GHC.Types.:
133+
@GHC.Types.Char
134+
(GHC.Types.C# 'f'#)
135+
(GHC.Base.build
136+
@GHC.Types.Char
137+
(\@b -> GHC.CString.unpackFoldrCString# @b "0d1"#)
138+
)
139+
140+
Then we re-do the cons after un-doing the original rewrite rule.
123141
-}
124142

125143
compileLiteral ::
@@ -149,14 +167,43 @@ tryStringLiteralAsBytes coreExpr = case coreExpr of
149167
GHC.Var isUnpackCStringUtf8 `GHC.App` GHC.Lit (GHC.LitString bytes)
150168
| GHC.getName isUnpackCStringUtf8 == GHC.unpackCStringUtf8Name ->
151169
Just bytes
152-
-- See Note [unpackFoldrCString#]
153-
GHC.Var build `GHC.App` _ `GHC.App` GHC.Lam _ (GHC.Var unpack `GHC.App` _ `GHC.App` expr)
154-
| GHC.getName build == GHC.buildName && GHC.getName unpack == GHC.unpackCStringFoldrName ->
155-
tryStringLiteralAsBytes expr
156-
-- GHC helpfully generates an empty list for the empty string literal instead of a 'LitString'
170+
171+
{- See Note [unpackFoldrCString#]
172+
173+
Example GHC Core expr this pattern matches:
174+
GHC.Base.build
175+
@GHC.Types.Char
176+
(\@b -> GHC.CString.unpackFoldrCString# @b "0d1"#)
177+
-}
178+
GHC.Var build
179+
`GHC.App` GHC.Type (GHC.TyConApp charTyCon _kindOrType)
180+
`GHC.App` GHC.Lam _ (GHC.Var unpack `GHC.App` _ `GHC.App` expr)
181+
| GHC.getName build == GHC.buildName
182+
, GHC.getName unpack == GHC.unpackCStringFoldrName
183+
, GHC.getName charTyCon == GHC.charTyConName ->
184+
tryStringLiteralAsBytes expr
185+
{-
186+
Example GHC Core expr this pattern matches:
187+
GHC.Types.: @GHC.Types.Char (GHC.Types.C# 'f'#) expr
188+
-}
189+
GHC.Var consId
190+
`GHC.App` GHC.Type (GHC.TyConApp charTyCon _kindOrType)
191+
`GHC.App` (GHC.Var cSharp `GHC.App` GHC.Lit (GHC.LitChar c))
192+
`GHC.App` expr
193+
| GHC.getName charTyCon == GHC.charTyConName
194+
, Just consDataCon <- GHC.isDataConId_maybe consId
195+
, GHC.consDataCon == consDataCon
196+
, Just charDataCon <- GHC.isDataConId_maybe cSharp
197+
, GHC.charDataCon == charDataCon ->
198+
BSC.cons c <$> tryStringLiteralAsBytes expr
199+
200+
-- GHC helpfully generates an empty list for the empty string literal instead
201+
-- of a 'LitString'
157202
GHC.Var nil `GHC.App` GHC.Type (GHC.tyConAppTyCon_maybe -> Just tc)
158-
| nil == GHC.dataConWorkId GHC.nilDataCon, GHC.getName tc == GHC.charTyConName ->
203+
| nil == GHC.dataConWorkId GHC.nilDataCon
204+
, GHC.getName tc == GHC.charTyConName ->
159205
Just mempty
206+
160207
-- Chase variable references! GHC likes to lift string constants to variables,
161208
-- that is not good for us!
162209
GHC.Var (GHC.maybeUnfoldingTemplate . GHC.realIdUnfolding -> Just unfolding) ->
@@ -201,10 +248,13 @@ If 'ByteString' contains a codepoint that is not in this range, the function wil
201248
-}
202249
utf8CodePointsAsBytes :: Compiling uni fun m ann => BS.ByteString -> m BS.ByteString
203250
utf8CodePointsAsBytes bs =
204-
case gracefullyDecodeUtf8Bytes (BS.unpack bs) of
205-
Just bytes -> pure $ BS.pack bytes
251+
case tryUtf8CodePointsAsBytes bs of
252+
Just bytes -> pure bytes
206253
Nothing -> throwPlain . CompilationError $
207254
"ByteString literal is expected to contain only codepoints in the range 0 - 255 (0x00 - 0xFF)"
255+
256+
tryUtf8CodePointsAsBytes :: BS.ByteString -> Maybe BS.ByteString
257+
tryUtf8CodePointsAsBytes = fmap BS.pack . gracefullyDecodeUtf8Bytes . BS.unpack
208258
where
209259
{-
210260
Why not use 'Data.Text.Encoding'?
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{-# LANGUAGE NoImplicitPrelude #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# OPTIONS_GHC -fno-full-laziness #-}
4+
{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-}
5+
{-# OPTIONS_GHC -fno-omit-interface-pragmas #-}
6+
{-# OPTIONS_GHC -fno-spec-constr #-}
7+
{-# OPTIONS_GHC -fno-specialise #-}
8+
{-# OPTIONS_GHC -fno-strictness #-}
9+
{-# OPTIONS_GHC -fno-unbox-small-strict-fields #-}
10+
{-# OPTIONS_GHC -fno-unbox-strict-fields #-}
11+
{-# OPTIONS_GHC -fplugin PlutusTx.Plugin #-}
12+
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.1.0 #-}
13+
14+
module ByteStringLiterals.Lib where
15+
16+
import PlutusTx.Builtins (BuiltinByteStringHex (..))
17+
18+
{-
19+
20+
PlutusTx.Builtins.HasOpaque.stringToBuiltinByteStringHex
21+
(GHC.Types.:
22+
@GHC.Types.Char
23+
(GHC.Types.C# 'f'#)
24+
(GHC.Base.build
25+
@GHC.Types.Char
26+
(\ (@b) -> GHC.CString.unpackFoldrCString# @b "0d1"#)))
27+
-}
28+
{-# INLINEABLE hex #-}
29+
hex :: BuiltinByteStringHex
30+
hex = "f0d1"

plutus-tx-plugin/test/ByteStringLiterals/Spec.hs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77

88
module ByteStringLiterals.Spec (tests) where
99

10+
import ByteStringLiterals.Lib qualified as Lib
1011
import Data.ByteString (ByteString)
1112
import Data.Char (chr)
1213
import Data.Foldable (for_)
1314
import Data.String (fromString)
1415
import Data.Text.Encoding qualified as TE
15-
import PlutusCore (someValue)
16+
import PlutusCore (DefaultUni (..), Some (..), ValueOf (..), someValue)
1617
import PlutusTx (CompiledCode, getPlcNoAnn)
1718
import PlutusTx.Builtins (BuiltinByteString, BuiltinByteStringHex, BuiltinByteStringUtf8,
1819
fromBuiltin)
@@ -21,8 +22,7 @@ import PlutusTx.Builtins.HasOpaque (stringToBuiltinByteString, stringToBuiltinBy
2122
import PlutusTx.TH (compile)
2223
import Test.Tasty (TestTree, testGroup)
2324
import Test.Tasty.HUnit (testCase, (@?=))
24-
import UntypedPlutusCore (DefaultFun, DefaultUni, NamedDeBruijn, Program (_progTerm),
25-
Term (Constant))
25+
import UntypedPlutusCore (DefaultFun, NamedDeBruijn, Program (_progTerm), Term (Constant))
2626

2727
tests :: TestTree
2828
tests =
@@ -36,6 +36,7 @@ tests =
3636
, test_CompileBuiltinByteStringLiteral_utf8
3737
, test_CompileBuiltinByteStringLiteral_stringToBuiltinByteStringUtf8
3838
, test_CompileBuiltinByteStringLiteral_hex
39+
, test_CompileBuiltinByteStringLiteral_hex_imported
3940
, test_CompileBuiltinByteStringLiteral_stringToBuiltinByteStringHex
4041
]
4142
]
@@ -148,7 +149,7 @@ test_CompileBuiltinByteStringLiteral_stringToBuiltinByteStringUtf8 =
148149

149150
test_CompileBuiltinByteStringLiteral_hex :: TestTree
150151
test_CompileBuiltinByteStringLiteral_hex =
151-
testCase "BuiltinByteStringHex" do
152+
testCase "BuiltinByteStringHex (local)" do
152153
term compiledLiteral @?= expectedUplc
153154
where
154155
compiledLiteral :: CompiledCode BuiltinByteStringHex =
@@ -188,6 +189,13 @@ test_CompileBuiltinByteStringLiteral_hex =
188189
\0f0e0d0c0b0a09080706050403020100"
189190
||]
190191
)
192+
193+
test_CompileBuiltinByteStringLiteral_hex_imported :: TestTree
194+
test_CompileBuiltinByteStringLiteral_hex_imported =
195+
testCase "BuiltinByteStringHex (imported)" $
196+
term $$(compile [||Lib.hex||])
197+
@?= Constant () (Some (ValueOf DefaultUniByteString "\240\209"))
198+
191199
test_CompileBuiltinByteStringLiteral_stringToBuiltinByteStringHex :: TestTree
192200
test_CompileBuiltinByteStringLiteral_stringToBuiltinByteStringHex =
193201
testCase "stringToBuiltinByteStringHex" do

0 commit comments

Comments
 (0)