Skip to content

Commit 1e7a0be

Browse files
authored
Fix Haskell version of certifier builtinEq (#7059)
1 parent 122453e commit 1e7a0be

File tree

4 files changed

+149
-36
lines changed

4 files changed

+149
-36
lines changed

plutus-executables/test/certifier/Test/Certifier/AST.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import UntypedPlutusCore
77
import AgdaTrace (mkAgdaTrace)
88
import MAlonzo.Code.VerifiedCompilation (runCertifierMain)
99

10+
import Data.Text.Encoding qualified as Text
1011
import Test.Tasty
1112
import Test.Tasty.HUnit
1213

@@ -80,9 +81,27 @@ testTrivialFailure1 =
8081
(mkConstant () (1 :: Integer))
8182
(mkConstant () (2 :: Integer))
8283

84+
testByteStringEqSuccess :: TestTree
85+
testByteStringEqSuccess =
86+
testFailure
87+
"bytestrings expected to not be equal"
88+
FloatDelay
89+
(mkConstant () (Text.encodeUtf8 "foo"))
90+
(mkConstant () (Text.encodeUtf8 "bar"))
91+
92+
testByteStringEqFailure :: TestTree
93+
testByteStringEqFailure =
94+
testSuccess
95+
"bytestrings expected to be equal"
96+
FloatDelay
97+
(mkConstant () (Text.encodeUtf8 "foo"))
98+
(mkConstant () (Text.encodeUtf8 "foo"))
99+
83100
astTests :: TestTree
84101
astTests =
85102
testGroup "certifier ast tests"
86103
[ testTrivialSuccess1
87104
, testTrivialFailure1
105+
, testByteStringEqSuccess
106+
, testByteStringEqFailure
88107
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
-----
3+
4+
- Fixed the runtime representation of `VerifiedCompilation.Equality.builtinEq`.
5+
- `DATA` equality now depends on `builtinEq` as well.

plutus-metatheory/src/Utils.lagda.md

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,12 @@ postulate ByteString : Set
176176
{-# COMPILE GHC ByteString = type BS.ByteString #-}
177177
178178
postulate
179-
eqByteString : ByteString → ByteString → Bool
180179
mkByteString : String → ByteString
180+
181+
-- Agda implementation should only be used as part of deciding builtin equality.
182+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
183+
eqByteString : ByteString → ByteString → Bool
184+
eqByteString _ _ = Bool.true
181185
{-# COMPILE GHC eqByteString = (==) #-}
182186
183187
```
@@ -253,31 +257,76 @@ data DATA : Set where
253257
{-# FOREIGN GHC import PlutusCore.Data as D #-}
254258
{-# COMPILE GHC DATA = data Data (D.Constr | D.Map | D.List | D.I | D.B) #-}
255259
256-
postulate eqDATA : DATA → DATA → Bool
260+
-- Agda implementation should only be used as part of deciding builtin equality.
261+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
262+
{-# TERMINATING #-}
263+
eqDATA : DATA → DATA → Bool
264+
eqDATA (ConstrDATA i₁ l₁) (ConstrDATA i₂ l₂) =
265+
(Relation.Nullary.isYes (i₁ Data.Integer.≟ i₂))
266+
Data.Bool.∧
267+
L.and (L.zipWith eqDATA (toList l₁) (toList l₂))
268+
eqDATA (ConstrDATA x x₁) (MapDATA x₂) = Bool.false
269+
eqDATA (ConstrDATA x x₁) (ListDATA x₂) = Bool.false
270+
eqDATA (ConstrDATA x x₁) (iDATA x₂) = Bool.false
271+
eqDATA (ConstrDATA x x₁) (bDATA x₂) = Bool.false
272+
eqDATA (MapDATA x) (ConstrDATA x₁ x₂) = Bool.false
273+
eqDATA (MapDATA m₁) (MapDATA m₂) =
274+
L.and
275+
(L.zipWith
276+
(λ (x₁ , y₁) (x₂ , y₂) → eqDATA x₁ x₂ Data.Bool.∧ eqDATA y₁ y₂)
277+
(toList m₁)
278+
(toList m₂)
279+
)
280+
eqDATA (MapDATA x) (ListDATA x₁) = Bool.false
281+
eqDATA (MapDATA x) (iDATA x₁) = Bool.false
282+
eqDATA (MapDATA x) (bDATA x₁) = Bool.false
283+
eqDATA (ListDATA x) (ConstrDATA x₁ x₂) = Bool.false
284+
eqDATA (ListDATA x) (MapDATA x₁) = Bool.false
285+
eqDATA (ListDATA x) (ListDATA x₁) = L.and (L.zipWith eqDATA (toList x) (toList x₁))
286+
eqDATA (ListDATA x) (iDATA x₁) = Bool.false
287+
eqDATA (ListDATA x) (bDATA x₁) = Bool.false
288+
eqDATA (iDATA x) (ConstrDATA x₁ x₂) = Bool.false
289+
eqDATA (iDATA x) (MapDATA x₁) = Bool.false
290+
eqDATA (iDATA x) (ListDATA x₁) = Bool.false
291+
eqDATA (iDATA i₁) (iDATA i₂) = Relation.Nullary.isYes (i₁ Data.Integer.≟ i₂)
292+
eqDATA (iDATA x) (bDATA x₁) = Bool.false
293+
eqDATA (bDATA x) (ConstrDATA x₁ x₂) = Bool.false
294+
eqDATA (bDATA x) (MapDATA x₁) = Bool.false
295+
eqDATA (bDATA x) (ListDATA x₁) = Bool.false
296+
eqDATA (bDATA x) (iDATA x₁) = Bool.false
297+
-- Warning: eqByteString is always trivially true at the Agda level.
298+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
299+
eqDATA (bDATA b₁) (bDATA b₂) = eqByteString b₁ b₂
257300
{-# COMPILE GHC eqDATA = (==) #-}
258301
259302
postulate Bls12-381-G1-Element : Set
260303
{-# FOREIGN GHC import qualified PlutusCore.Crypto.BLS12_381.G1 as G1 #-}
261304
{-# COMPILE GHC Bls12-381-G1-Element = type G1.Element #-}
262305
263-
postulate
264-
eqBls12-381-G1-Element : Bls12-381-G1-Element → Bls12-381-G1-Element → Bool
306+
-- Agda implementation should only be used as part of deciding builtin equality.
307+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
308+
eqBls12-381-G1-Element : Bls12-381-G1-Element → Bls12-381-G1-Element → Bool
309+
eqBls12-381-G1-Element _ _ = Bool.true
265310
{-# COMPILE GHC eqBls12-381-G1-Element = (==) #-}
266311
267312
postulate Bls12-381-G2-Element : Set
268313
{-# FOREIGN GHC import qualified PlutusCore.Crypto.BLS12_381.G2 as G2 #-}
269314
{-# COMPILE GHC Bls12-381-G2-Element = type G2.Element #-}
270315
271-
postulate
272-
eqBls12-381-G2-Element : Bls12-381-G2-Element → Bls12-381-G2-Element → Bool
316+
-- Agda implementation should only be used as part of deciding builtin equality.
317+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
318+
eqBls12-381-G2-Element : Bls12-381-G2-Element → Bls12-381-G2-Element → Bool
319+
eqBls12-381-G2-Element _ _ = Bool.true
273320
{-# COMPILE GHC eqBls12-381-G2-Element = (==) #-}
274321
275322
postulate Bls12-381-MlResult : Set
276323
{-# FOREIGN GHC import qualified PlutusCore.Crypto.BLS12_381.Pairing as Pairing #-}
277324
{-# COMPILE GHC Bls12-381-MlResult = type Pairing.MlResult #-}
278325
279-
postulate
280-
eqBls12-381-MlResult : Bls12-381-MlResult → Bls12-381-MlResult → Bool
326+
-- Agda implementation should only be used as part of deciding builtin equality.
327+
-- See "Decidable Equality of Builtins" in "VerifiedCompilation.Equality".
328+
eqBls12-381-MlResult : Bls12-381-MlResult → Bls12-381-MlResult → Bool
329+
eqBls12-381-MlResult _ _ = Bool.true
281330
{-# COMPILE GHC eqBls12-381-MlResult = (==) #-}
282331
```
283332

plutus-metatheory/src/VerifiedCompilation/Equality.lagda.md

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -148,40 +148,51 @@ decEq-TyTag (_⊢♯.pair t t₁) (_⊢♯.pair t' t'') with (t ≟ t') ×-dec (
148148
... | no ¬pq = no λ { refl → ¬pq (refl , refl) }
149149
150150
```
151-
The equality of the semantics of constants will depend on the equality of
152-
the underlying types, so this can leverage the standard library decision
153-
procedures.
151+
# Decidable Equality of Builtins
154152

155-
```
156-
record HsEq (A : Set) : Set where
157-
field
158-
hsEq : A → A → Agda.Builtin.Bool.Bool
153+
We need to decide equality between our builtin types. This is tricky because
154+
this needs to be done at both the Agda type-checking time and at runtime, while
155+
each stage has a completely different representation of the types.
159156

160-
open HsEq {{...}} public
157+
## Type-checking time vs runtime
161158

162-
postulate
163-
magicNeg : ∀ {A : Set} {a b : A} → ¬ a ≡ b
159+
In Agda, the types are postulated, which means that at type-checking time we
160+
may only rely on Agda's unification algorithm to decide equality. This can be
161+
done by matching on `refl`, which checks whether the left hand side and the
162+
right hand side of `` are definitionally equal. However, this does not translate
163+
to the runtime stage, since at runtime the values which the `` type depends on
164+
are erased. Therefore, we need to somehow "inject" a Haskell equality function which
165+
triggers only at the runtime stage.
164166

165-
magicBoolDec : {A : Set} → {a b : A} → Agda.Builtin.Bool.Bool → Dec (a ≡ b)
166-
magicBoolDec true = yes primTrustMe
167-
magicBoolDec false = no magicNeg
168-
```
167+
## Why not just implement the builtin types in Agda?
168+
169+
The problem is that Agda's FFI only allows non-postulated Agda types which are
170+
representationally equivalent to the Haskell types they compile to. If we were to
171+
implement the types in Agda, they would need to be equivalent to the highly optimized
172+
and complicated Haskell types, and this is not feasible.
173+
174+
We also cannot de-couple the Agda types from the Haskell types because the Agda
175+
specification of UPLC is also used in conformance testing.
176+
177+
## Using the quirks of the FFI to our advantage
169178

170-
Our builtins types and functions are postulated. In order to decide equality
171-
we rely on Agda's notion of definitional equality.
179+
Agda's FFI machinery allows us to define functions with different runtime
180+
and type-checking definitions (see the warning at https://agda.readthedocs.io/en/v2.7.0.1/language/foreign-function-interface.html#using-haskell-functions-from-agda).
181+
We are still constrained by the type, which needs to agree between the two
182+
stages, so we can't just define the two implementations arbitrarily.
172183

173-
The definition of `builtinEq` might seem strange, but what happens is that
174-
matching on `refl` triggers Agda's unification algorithm, which checks whether
175-
the two terms are definitionally equal.
184+
The simplest solution is to provide separate type-checking time and runtime definitions
185+
for the instances of `HsEq`. During type-checking, the functions are essentially no-ops
186+
by always returning `true`, while at runtime they defer to the Haskell implementation of
187+
equality for each type. At type-checking time, we rely on matching on `refl` to defer to
188+
Agda's unification algorithm, while at runtime, the matching on `refl` becomes a no-op.
176189

177-
For example: for `builtinEq (mkByteString "foo") (mkByteString "foo")` the two terms
178-
are structurally equal so unification will succeed, and the function will return
179-
`yes refl`, while `builtinEq (mkByteString "foo") (mkByteString "bar")` will get
180-
stuck because unification does not succeed.
181190
```
182-
builtinEq : {A : Set} → Binary.Decidable {A = A} _≡_
183-
builtinEq {A} x y with primTrustMe {Agda.Primitive.lzero} {A} {x} {y}
184-
... | refl = yes refl
191+
record HsEq (A : Set) : Set where
192+
field
193+
hsEq : A → A → Agda.Builtin.Bool.Bool
194+
195+
open HsEq {{...}} public
185196
186197
instance
187198
HsEqBytestring : HsEq U.ByteString
@@ -192,14 +203,43 @@ instance
192203
HsEqBlsG2 = record { hsEq = U.eqBls12-381-G2-Element }
193204
HsEqBlsMlResult : HsEq U.Bls12-381-MlResult
194205
HsEqBlsMlResult = record { hsEq = U.eqBls12-381-MlResult }
206+
HsEqDATA : HsEq U.DATA
207+
HsEqDATA = record { hsEq = U.eqDATA }
208+
209+
```
210+
211+
## An example
212+
213+
Let's look at the behavior of `builtinEq (mkByteString "foo") (mkByteString "foo")` vs
214+
`builtinEq (mkByteString "foo") (mkByteString "bar")`.
215+
216+
At type-checking time, if the two bytestrings are definitionally equal unification will succeed,
217+
and the function will return `yes refl`. There is no way to return `no` because there is
218+
no way to prove that the two terms are not equal without extra information about the
219+
`ByteString` type. But this is enough to make Agda not succesfully type-check the program,
220+
since it gets stuck while trying to normalize `primTrustMe`.
221+
222+
At runtime, `hsEq` will defer to the Haskell implementation of bytestring equality, and return
223+
the correct result based on that. In the `yes` case, matching on `refl` will be a no-op,
224+
while in the `no` case, we return a phony negative proof. This is safe to do because we're
225+
at runtime and the proof gets erased anyway.
226+
227+
```
228+
postulate
229+
magicNeg : ∀ {A : Set} {a b : A} → ¬ a ≡ b
230+
231+
builtinEq : {A : Set} {{_ : HsEq A}} → Binary.Decidable {A = A} _≡_
232+
builtinEq {A} x y with hsEq x y
233+
... | false = no magicNeg
234+
... | true with primTrustMe {Agda.Primitive.lzero} {A} {x} {y}
235+
... | refl = yes refl
195236
196237
decEq-⟦ _⊢♯.atomic AtomicTyCon.aInteger ⟧tag = Data.Integer.Properties._≟_
197238
decEq-⟦ _⊢♯.atomic AtomicTyCon.aBytestring ⟧tag = builtinEq
198239
decEq-⟦ _⊢♯.atomic AtomicTyCon.aString ⟧tag = Data.String.Properties._≟_
199240
decEq-⟦ _⊢♯.atomic AtomicTyCon.aUnit ⟧tag = Data.Unit.Properties._≟_
200241
decEq-⟦ _⊢♯.atomic AtomicTyCon.aBool ⟧tag = Data.Bool.Properties._≟_
201-
-- TODO(https://github.com/IntersectMBO/plutus-private/issues/1528): why does this use magicBoolDec? surely it can be implemented correctly
202-
decEq-⟦ _⊢♯.atomic AtomicTyCon.aData ⟧tag v v₁ = magicBoolDec (U.eqDATA v v₁)
242+
decEq-⟦ _⊢♯.atomic AtomicTyCon.aData ⟧tag = builtinEq
203243
decEq-⟦ _⊢♯.atomic AtomicTyCon.aBls12-381-g1-element ⟧tag = builtinEq
204244
decEq-⟦ _⊢♯.atomic AtomicTyCon.aBls12-381-g2-element ⟧tag = builtinEq
205245
decEq-⟦ _⊢♯.atomic AtomicTyCon.aBls12-381-mlresult ⟧tag = builtinEq

0 commit comments

Comments
 (0)