Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better errors for literal ranges in PlutusTx (PLT-8174) #5619

Merged
merged 37 commits into from
Nov 23, 2023

Conversation

kwxm
Copy link
Contributor

@kwxm kwxm commented Nov 9, 2023

[See also issues #5589 and #3395].

Suppose you have this code in a Haskell program:

   let !_ = $$(compile [|| [1..99] :: [Integer] ||])

The plugin currently gives you a 186-line error message like this:

GHC Core to PLC plugin: Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable GHC.Num.Integer.integerGt#
                               No unfolding
Context: Compiling expr: GHC.Num.Integer.integerGt#
Context: Compiling expr: GHC.Num.Integer.integerGt# x1
Context: Compiling expr: GHC.Num.Integer.integerGt# x1 y1
Context: Compiling expr: case GHC.Num.Integer.integerGt# x1 y1 of {
                           __DEFAULT -> c x1 (go3 (GHC.Num.Integer.integerAdd x1 1));
                           1# -> n
...
<lots of nested context information>
...
Context: Compiling definition of: GHC.Enum.$fEnumInteger_$cenumFromTo
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo 1
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo 1 99
Context: Compiling expr at "main:Main:(26,15)-(26,52)"

This happens because the range is desugared to a call to enumFromTo in the Enum class, but things are complicated by the fact that in the case of Integer there is extensive inlining and rewriting.

This PR improves things slightly by spotting uses of enumFromTo and related functions and suggesting that the user should use PlutusTx.enumFromTo instead. I've also added PlutusTx.enumFromThenTo for ranges like [1, 5..101] with non-unit steps.

With this change the above error message is replaced by

GHC Core to PLC plugin: Error: Unsupported feature: Literal ranges are not supported: please use PlutusTx.Enum.enumFromTo or PlutusTx.Enum.enumFromThenTo
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo 1
Context: Compiling expr: GHC.Enum.$fEnumInteger_$cenumFromTo 1 99
Context: Compiling expr at "main:Main:(26,15)-(26,52)"

This is an improvement, but is still not ideal: if the range is nested inside multiple functions then you'll still get lots of nested contexts with the final line (which is the only one containing a source location) pointing to the splice, which may be a long way from where the range occurs. I think we should merge this for now and think about ways to improve the situation.

Possible improvements

I thought about getting the plugin to replace occurrences of enumFromTo with PlutusTx.enumFromTo. This is tricky for a number of reasons:

  • Because of the inlining/rewriting for Integer some other function may be called and it's difficult to work out what's going on.
  • We'd need a GHC Name for PlutusTx.enumFromTo: those are quite complicated (they include unique IDs, for example) and I'm not sure how to get hold of one.
  • Maybe it's just not a good idea to silently replace a standard Haskell function with our own version.

I looked at trying to improve the context information as well, but it looked as if that'd take quite a lot of work.

  • The plugin provides a PlutusTx.Plugin:context-level option which looks as if it should let you change the amount of context information, but I couldn't get it to work. I think it may be getting overridden by calls to traceCompilation like this.
  • Is there any way to get accurate location information for stuff that's reachable transitively from the splice? You can kind of see what might be going on, but there can be huge amounts of GHC Core to work through. See issues Compilation errors report do not point to the location  #5589 and Use of ranges in Plutus #3395 for complaints about this.

@kwxm kwxm added the Plinth label Nov 9, 2023
@kwxm kwxm requested review from michaelpj and zliu41 November 9, 2023 11:42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change anything in this file but appear to have reformatted it accidentally. I don't think it'll do any harm.

enumFromTo x y
| x > y = []
| otherwise = x : enumFromTo (succ x) y
enumFromTo x lim
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed y to lim so that it looks more like what's in GHC.Enum. That changed a lot of golden files too.

@@ -200,12 +200,12 @@ let
in
letrec
!`$fEnumBool_$cenumFromTo` : integer -> integer -> List integer
= \(x : integer) (y : integer) ->
= \(x : integer) (lim : integer) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See later for an explanation of why all of these have changed.

@@ -115,7 +115,7 @@ executable gen-plugin-opts-doc

default-language: Haskell2010

test-suite plutus-tx-tests
test-suite plutus-tx-plugin-tests
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because plutus-tx contains plutus-tx-test and I was getting very confused.

| Just m <- GHC.nameModule_maybe n
, GHC.moduleNameString (GHC.moduleName m) == "GHC.Enum" =
("$fEnum" `isPrefixOf` methodName &&
( "_$cenumFromTo" `isSuffixOf` methodName -- [1..100]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any better way of getting names of particular instances of methods like enumFromTo? This was the best I could do and I'm not sure how robust it will be. I suppose that if the names that GHC produces change then the tests will start to fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is too bad. I hadn't realized that GHC might specialize enumFromTo.

The answer to your question is, unfortunately, no. There's no way to prevent GHC from specializing a method. It is only possible to disable user written rules, not builtin rules.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should match on the specialized name. I don't know if we can guarantee that that isn't an arbitrarily messed up version 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass some information about GHC names through to the compiler in NameInfo in the CompileContext. Then you can use getThing to get the TypedThing and getName on that to get the Name. See how the stuff in Builitns works.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass some information about GHC names through to the compiler in NameInfo in the CompileContext. Then you can use getThing to get the TypedThing and getName on that to get the Name. See how the stuff in Builitns works.

Maybe we can come back to this in a later PR. I've run out of JIRA points on this.

Copy link
Contributor Author

@kwxm kwxm Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should match on the specialized name. I don't know if we can guarantee that that isn't an arbitrarily messed up version 🤔

That's what I was asking about in my original comment. Can we actually get at the original name, especially after the inlining/rewriting? Is that what NameInfo is about?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, NameInfo is just for getting an actual GHC Name instead of string matching. I'm not sure it will match up if it has in fact been specialized. I'm not sure what we do in other cases, perhaps we just fail to spot stuff 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's not too bad since we're failing instead of silently compiling differently.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess since this is inherently heuristic rather than matching a precise name there's no point doing the NameInfo stuff and this is fine.

@kwxm
Copy link
Contributor Author

kwxm commented Nov 9, 2023

The checkScriptContextEquality test in plutus-benchmark is failing because its CPU budget has increased slightly. I'm finding it difficult to imagine why this might be happening.

Edit: the budget increase seems to be due to the addition of enumFromThenTo to Plutus.tx.Enum. Maybe I should remove that again since it probably won't be used very often.

Edit: I've updated the test results, but we could still remove enumFromThenTo.

@@ -2,7 +2,6 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

{-# OPTIONS_GHC -fplugin PlutusTx.Plugin #-}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to do with this PR: I'm just taking the opportunity to remove it while I'm here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I'm going to revert that. It may be causing problems with at least one GHC version.

Copy link
Member

@zliu41 zliu41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

| Just m <- GHC.nameModule_maybe n
, GHC.moduleNameString (GHC.moduleName m) == "GHC.Enum" =
("$fEnum" `isPrefixOf` methodName &&
( "_$cenumFromTo" `isSuffixOf` methodName -- [1..100]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is too bad. I hadn't realized that GHC might specialize enumFromTo.

The answer to your question is, unfortunately, no. There's no way to prevent GHC from specializing a method. It is only possible to disable user written rules, not builtin rules.

-- by looking for occurrences of GHC.Enum.enumFromTo and similar functions; the same error
-- occurs if these functions are used explicitly.
| isProbablyBoundedRange n ->
throwPlain $ UnsupportedError "Literal ranges are not supported: please use PlutusTx.Enum.enumFromTo or PlutusTx.Enum.enumFromThenTo"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't know for sure that the user used list range syntax. You can instead say something like: "Unsupported feature: GHC.Enum.Enum. You probably used list range syntax, which is not currently supported. You can use PlutusTx.Enum.Enum instead".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't know for sure that the user used list range syntax.

Yes, I did worry about that but decided to go for the shorter version to avoid a giant string in the source code. I'll change it.


-- Tests for literal ranges (and the corresponding methods in GHC.Enum). These
-- should all fail with informative error messages.
rangeEnumFromTo :: CompiledCode [Integer]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests.

| Just m <- GHC.nameModule_maybe n
, GHC.moduleNameString (GHC.moduleName m) == "GHC.Enum" =
("$fEnum" `isPrefixOf` methodName &&
( "_$cenumFromTo" `isSuffixOf` methodName -- [1..100]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should match on the specialized name. I don't know if we can guarantee that that isn't an arbitrarily messed up version 🤔

| Just m <- GHC.nameModule_maybe n
, GHC.moduleNameString (GHC.moduleName m) == "GHC.Enum" =
("$fEnum" `isPrefixOf` methodName &&
( "_$cenumFromTo" `isSuffixOf` methodName -- [1..100]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass some information about GHC names through to the compiler in NameInfo in the CompileContext. Then you can use getThing to get the TypedThing and getName on that to get the Name. See how the stuff in Builitns works.

-- | Construct a list from the given range (corresponds to [a,b..c]). This
-- has the same semantics as the Haskell version,so if a==b and c>=b then you
-- get an infinite list, which you probably don't want in Plutus Core.
enumFromThenTo :: a -> a -> a -> [a]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should add this. It's not that useful and it makes the dictionaries bigger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should add this. It's not that useful and it makes the dictionaries bigger.

Yeah, I wondered about that. I thought I might as well add it while I was looking at this, but it didn't initially occur to me that it would make the dictionary bigger.

@kwxm
Copy link
Contributor Author

kwxm commented Nov 23, 2023

I think I'd like to merge this to get it out of the way. There are a number of things I'd like to revisit at a later date though: see the original PR comments.

@kwxm kwxm enabled auto-merge (squash) November 23, 2023 10:49
@kwxm kwxm merged commit b923816 into master Nov 23, 2023
6 checks passed
@kwxm kwxm deleted the kwxm/plugin/better-range-errors branch November 23, 2023 13:48
@bezirg
Copy link
Contributor

bezirg commented Dec 4, 2023

@kwxm @michaelpj Have you looked into using RebindableSyntax instead?

From https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/rebindable_syntax.html :

So the [RebindableSyntax](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/rebindable_syntax.html#extension-RebindableSyntax) extension causes the following pieces of built-in syntax to refer to whatever is in scope, not the Prelude versions:

List notation, such as [x,y] or [m..n] can also be treated via rebindable syntax if you use -XOverloadedLists; see [Overloaded lists](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/overloaded_lists.html#overloaded-lists).

It should make the list notation work for PlutusTx, at least on paper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants