Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions doc/notes/plutus-tx-plugin/NO_SIMPLE_PASS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Investigation: Disabling mkSimplPass in PlutusTx Plugin

## Context

This investigation was conducted to understand whether disabling the `mkSimplPass` in the PlutusTx plugin would improve error messages for stage violations, specifically the confusing "Cannot construct a value of type: BuiltinUnit" error reported in [plutus-private#1626](https://github.com/IntersectMBO/plutus-private/issues/1626).

## Background

The `mkSimplPass` was originally added to work around [GHC #16615](https://gitlab.haskell.org/ghc/ghc/-/issues/16615), where local bindings lack unfoldings when Core plugins run early in the compilation pipeline. The simplifier pass with `sm_pre_inline = True` generates these unfoldings by inlining bindings that occur exactly once unconditionally.

### Why mkSimplPass Was Needed

From Note [GHC.sm_pre_inline] in the code:
- The plugin requires certain functions to be fully applied
- Example: it can handle `noinline @(String -> BuiltinString) stringToBuiltinString "a"` but not `let f = noinline @(String -> BuiltinString) stringToBuiltinString in f "a"`
- Pre-inlining solves this by inlining single-use bindings before the plugin runs

## Investigation Method

1. Disabled `mkSimplPass` in `PlutusTx.Plugin.install`
2. Attempted to build the plutus-tx-plugin test suite
3. Categorized and analyzed all compilation failures
4. Assessed whether failures could be fixed without the simplifier pass

## Key Findings

### ✅ Primary Goal Achieved: Better Error Messages

**Before (with mkSimplPass):**
```
Error: Unsupported feature: Cannot construct a value of type: BuiltinUnit
Context: Compiling expr: BuiltinUnit
Context: Compiling expr: BuiltinUnit ipv
```

**After (without mkSimplPass):**
```
Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable validator1
No unfolding
Context: Compiling expr: validator1
Context: Compiling expr at: test/BuiltinUnit/Spec.hs:74:11-36
```

The new error message clearly indicates the stage violation problem and points to the exact location, making it much easier for users to understand and fix.

### Failure Categories

Without `mkSimplPass`, we observed three main categories of failures:

#### 1. Stage Violations (✅ Fixed)
- **BuiltinUnit test cases**: Now produce clear "No unfolding" errors instead of confusing builtin constructor errors
- **This is the desired behavior** - code that violates PlutusTx compilation rules should fail with meaningful messages

#### 2. Missing Unfoldings for INLINEABLE Functions (🔧 Potentially Fixable)
```
Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable Plugin.Coverage.Spec.fun
OtherCon []
```
- **Root cause**: GHC #16615 - local bindings lack unfoldings without simplifier
- **Pattern**: Functions marked `{-# INLINEABLE #-}` but defined after their use site
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's probably also the case for function marked inlineable but defined on the other module.

- **Solutions**:
- Move functions to top-level
- Generate minimal unfoldings during desugaring (GHC enhancement needed)
- Create lightweight unfolding pass (lighter than full simplifier)
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this possible without making changes to GHC itself? From what I can see(which can be very wrong), we can only really change the plugin pass and the plugin pass doesn't have access to other stages of compilation.

This would be very easy if we have whole compilation pipeline under control.


#### 3. String Literal Processing Issues (🔧 Potentially Fixable)
```
Error: Unsupported feature: Literal string (maybe you need to use OverloadedStrings)
Context: Compiling expr: "f0d1"#
```
- **Root cause**: String literals not being processed properly without pre-inlining
- **Pattern**: Hex string literals in ByteString contexts
- **Solutions**:
- Pre-process string literals before plugin runs
- Better handling of `OverloadedStrings` in plugin
- Add specific support for hex string literal patterns

## Feasibility Assessment

### ✅ **Highly Feasible**

The investigation shows that disabling `mkSimplPass` is **feasible** for several reasons:

1. **Primary goal achieved**: Stage violations now produce clear, actionable error messages
2. **Most failures are fixable**: The breaking changes are due to missing unfoldings, not fundamental incompatibilities
3. **Targeted solutions possible**: Each failure category has potential solutions that don't require the full complexity of the current simplifier pass
4. **Performance benefits**: Removing the simplifier pass would improve compilation performance

### Recommended Approach

1. **Phase 1**: Implement targeted fixes for the most common failure patterns:
- Add lightweight unfolding generation for `INLINEABLE` functions
- Improve string literal handling
- Update problematic test code organization

2. **Phase 2**: Gradually remove `mkSimplPass` once core issues are resolved:
- Start with a flag to disable the pass for testing
- Monitor performance and correctness
- Full removal once confidence is high

3. **Phase 3**: Enhance error messages further:
- Add specific detection and messaging for stage violations
- Provide suggestions for fixing common problems

## Impact Analysis

### Benefits
- **Much clearer error messages** for stage violations
- **Better compilation performance** (no unnecessary simplification)
- **Reduced complexity** in the plugin pipeline
- **Better alignment** with PlutusTx compilation model

### Risks
- **Some legitimate code patterns may break** initially
- **Requires careful migration** of existing code
- **May need GHC enhancements** for optimal solution (addressing GHC #16615)

## Conclusion

Disabling `mkSimplPass` is a **promising approach** that achieves the primary goal of improving error messages while being technically feasible. The investigation demonstrates that most failures can be addressed through targeted solutions rather than requiring the full simplifier pass.

The trade-off between better error messages and some initial compatibility issues is favorable, especially given that the breaking patterns often represent code that violates PlutusTx compilation rules anyway.

## Test Results Summary

- **BuiltinUnit.Spec**: ✅ Compiles successfully, produces clear error messages
- **Plugin tests with unfolding issues**: ❌ Need targeted fixes
- **String literal tests**: ❌ Need preprocessing improvements
- **Overall plugin functionality**: 🔧 Maintainable with targeted improvements

---

*Investigation conducted on: 2025-01-25*
*Branch: yura/Cannot-construct-a-value-of-type*
*Related Issues: [plutus-private#1626](https://github.com/IntersectMBO/plutus-private/issues/1626), [GHC #16615](https://gitlab.haskell.org/ghc/ghc/-/issues/16615)*
1 change: 1 addition & 0 deletions plutus-tx-plugin/plutus-tx-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ test-suite plutus-tx-plugin-tests
Budget.WithGHCOptimisations
Budget.WithoutGHCOptimisations
BuiltinList.Budget.Spec
BuiltinUnit.Spec
ByteStringLiterals.Lib
ByteStringLiterals.Spec
CallTrace.Lib
Expand Down
54 changes: 50 additions & 4 deletions plutus-tx-plugin/src/PlutusTx/Compiler/Type.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import PlutusTx.Compiler.Types
import PlutusTx.Compiler.Utils
import PlutusTx.PIRTypes

import Language.Haskell.TH.Syntax qualified as TH
import PlutusTx.Builtins.Internal (BuiltinArray, BuiltinBool, BuiltinByteString, BuiltinData,
BuiltinList, BuiltinPair, BuiltinString, BuiltinUnit)

import GHC.Builtin.Types.Prim qualified as GHC
import GHC.Core.FamInstEnv qualified as GHC
import GHC.Core.Multiplicity qualified as GHC
Expand Down Expand Up @@ -298,6 +302,42 @@ ghcStrictnessNote =
GHC.<+> "'-fno-strictness', '-fno-specialise', '-fno-spec-constr',"
GHC.<+> "'-fno-unbox-strict-fields', or '-fno-unbox-small-strict-fields'."

-- | Check if a type constructor represents a builtin type
-- that indicates a potential stage violation
isBuiltinTypeCon :: (Compiling uni fun m ann) => GHC.TyCon -> m Bool
isBuiltinTypeCon tc = do
let tcOccName = GHC.occNameString (GHC.getOccName tc)
isFromBuiltinsInternal =
"PlutusTx.Builtins.Internal" ==
GHC.moduleNameString (GHC.moduleName (GHC.nameModule (GHC.getName tc)))
builtinTypeNames = TH.nameBase <$>
[ ''BuiltinUnit
, ''BuiltinData
, ''BuiltinBool
, ''BuiltinString
, ''BuiltinByteString
, ''BuiltinList
, ''BuiltinPair
, ''BuiltinArray
]
pure $ isFromBuiltinsInternal && tcOccName `elem` builtinTypeNames

stageViolationError :: GHC.TyCon -> GHC.SDoc
stageViolationError tc =
"Cannot construct a value of type:" GHC.<+> GHC.ppr tc GHC.$+$
"" GHC.$+$
"This error often indicates a stage violation in PlutusTx compilation." GHC.$+$
"Variables inside compile quotations must be either:" GHC.$+$
" • Top-level variables, or" GHC.$+$
" • Bound inside the quotation itself" GHC.$+$
"" GHC.$+$
"Common causes:" GHC.$+$
" • Using a function defined in a 'where' clause: move it to the top level" GHC.$+$
" • Referencing local variables from outside the quotation" GHC.$+$
"" GHC.$+$
"Original error context:" GHC.<+> GHC.ppr tc GHC.$+$
ghcStrictnessNote

-- | Get the constructors of the given 'TyCon' as PLC terms.
getConstructors :: (CompilingDefault uni fun m ann) => GHC.TyCon -> m [PIRTerm uni fun]
getConstructors tc = do
Expand All @@ -306,9 +346,12 @@ getConstructors tc = do
maybeConstrs <- PIR.lookupConstructors (LexName $ GHC.getName tc)
case maybeConstrs of
Just constrs -> pure constrs
Nothing ->
Nothing -> do
isBuiltin <- isBuiltinTypeCon tc
throwSd UnsupportedError $
"Cannot construct a value of type:" GHC.<+> GHC.ppr tc GHC.$+$ ghcStrictnessNote
if isBuiltin
then stageViolationError tc
else "Cannot construct a value of type:" GHC.<+> GHC.ppr tc GHC.$+$ ghcStrictnessNote

-- | Get the matcher of the given 'TyCon' as a PLC term
getMatch :: (CompilingDefault uni fun m ann) => GHC.TyCon -> m (PIRTerm uni fun)
Expand All @@ -318,9 +361,12 @@ getMatch tc = do
maybeMatch <- PIR.lookupDestructor annMayInline (LexName $ GHC.getName tc)
case maybeMatch of
Just match -> pure match
Nothing ->
Nothing -> do
isBuiltin <- isBuiltinTypeCon tc
throwSd UnsupportedError $
"Cannot case on a value on type:" GHC.<+> GHC.ppr tc GHC.$+$ ghcStrictnessNote
if isBuiltin
then stageViolationError tc
else "Cannot case on a value on type:" GHC.<+> GHC.ppr tc GHC.$+$ ghcStrictnessNote

{-| Get the matcher of the given 'Type' (which must be equal to a type constructor application)
as a PLC term instantiated for the type constructor argument types.
Expand Down
106 changes: 106 additions & 0 deletions plutus-tx-plugin/test/BuiltinUnit/Spec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE Strict #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -ddump-simpl-iterations -dsuppress-all #-}
{-# OPTIONS_GHC -fno-float-in #-}
{-# OPTIONS_GHC -fno-full-laziness #-}
{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-}
{-# OPTIONS_GHC -fno-local-float-out #-}
{-# OPTIONS_GHC -fno-omit-interface-pragmas #-}
{-# OPTIONS_GHC -fno-spec-constr #-}
{-# OPTIONS_GHC -fno-specialise #-}
{-# OPTIONS_GHC -fno-strictness #-}
{-# OPTIONS_GHC -fno-unbox-small-strict-fields #-}
{-# OPTIONS_GHC -fno-unbox-strict-fields #-}
{-# OPTIONS_GHC -fplugin PlutusTx.Plugin #-}

module BuiltinUnit.Spec where

import PlutusTx.Prelude
import Prelude (IO, seq)

import Control.Lens (view)
import PlutusTx (CompiledCode, compile, getPlcNoAnn)
import PlutusTx.Builtins.Internal (unitval)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, testCase)
import UntypedPlutusCore (progTerm)

tests :: TestTree
tests =
testGroup
"BuiltinUnit"
[ testCase "error ()" do assertCompiledTerm code1
, testCase "unitval" do assertCompiledTerm code2
, testCase "locally defined constructor" do assertCompiledTerm code3
, testCase "toOpaque ()" do assertCompiledTerm code4
]

{- GHC Core after simplification:

code1 = case error () of
validator1_X0 { BuiltinUnit ipv_smFH -> plc Proxy validator1_X0 }

code2 = case unitval of
validator2_X0 { BuiltinUnit ipv_smFJ -> plc Proxy validator2_X0 }

code3 = case unitval of
builtinUnit_X0 { BuiltinUnit ipv_smFL -> plc Proxy builtinUnit_X0 }

code4 = case toOpaque $fHasToOpaque()BuiltinUnit () of
validator4_X0 { BuiltinUnit ipv_smGp -> plc Proxy validator4_X0 }

Compilation error:

<no location info>: error:
GHC Core to PLC plugin:
Error: Unsupported feature:
Cannot construct a value of type: BuiltinUnit
Note: GHC can generate these unexpectedly, you may need
'-fno-strictness', '-fno-specialise', '-fno-spec-constr',
'-fno-unbox-strict-fields', or '-fno-unbox-small-strict-fields'.
Context: Compiling expr: BuiltinUnit
Context: Compiling expr: BuiltinUnit ipv
Context: Compiling definition of: validator1
Context: Compiling expr: validator1
Context: Compiling expr at: test/BuiltinUnit/Spec.hs:75:11-36
Context: Compiling expr: validator1
-}

code1 :: CompiledCode BuiltinUnit
code1 = $$(compile [||validator1||])
where
validator1 :: BuiltinUnit
validator1 = PlutusTx.Prelude.error ()
{-# INLINEABLE validator1 #-}

code2 :: CompiledCode BuiltinUnit
code2 = $$(compile [||validator2||])
where
validator2 :: BuiltinUnit
validator2 = unitval
{-# INLINEABLE validator2 #-}

code3 :: CompiledCode BuiltinUnit
code3 = $$(compile [||validator3||])
where
validator3 :: BuiltinUnit
validator3 = builtinUnit
{-# INLINEABLE validator3 #-}

builtinUnit :: BuiltinUnit
builtinUnit = unitval
{-# INLINEABLE builtinUnit #-}

code4 :: CompiledCode BuiltinUnit
code4 = $$(compile [||validator4||])
where
validator4 :: BuiltinUnit
validator4 = toOpaque ()
{-# INLINEABLE validator4 #-}

assertCompiledTerm :: CompiledCode a -> Assertion
assertCompiledTerm code = view progTerm (getPlcNoAnn code) `seq` return @IO ()
2 changes: 2 additions & 0 deletions plutus-tx-plugin/test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AssocMap.Spec qualified as AssocMap
import Blueprint.Tests qualified
import Budget.Spec qualified as Budget
import BuiltinList.Budget.Spec qualified as BuiltinList.Budget
import BuiltinUnit.Spec qualified as BuiltinUnit
import ByteStringLiterals.Spec qualified as ByteStringLiterals
import CallTrace.Spec qualified as CallTrace
import DataList.Budget.Spec qualified as DataList.Budget
Expand Down Expand Up @@ -61,5 +62,6 @@ tests =
, embed AssocMap.propertyTests
, embed List.propertyTests
, Array.smokeTests
, embed BuiltinUnit.tests
, CallTrace.tests
]