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

[ new ] totality checking can look under constructors #3362

Merged
merged 5 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ This CHANGELOG describes the merged but unreleased changes. Please see [CHANGELO
* The compiler now parses `~x.fun` as unquoting `x` rather than `x.fun`
and `~(f 5).fun` as unquoting `(f 5)` rather than `(f 5).fun`.

* Totality checking will now look under data constructors, so `Just xs` will
be considered smaller than `Just (x :: xs)`.

* LHS of `with`-applications are parsed as `PWithApp` instead of `PApp`. As a
consequence, `IWithApp` appears in `TTImp` values in elaborator scripts instead
of `IApp`, as it should have been.
Expand Down
7 changes: 7 additions & 0 deletions docs/source/reference/pragmas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ definition.
Set how deep nested ambiguous names can be before Idris gives up. The default is 3, increashing this
will effect compiler performance. For more details, see :ref:`ambiguous-name-resolution`.

``%totality_depth``
-------------------

Set the number of matching constructors Idris will look under when checking totality. For instance
`Just xs` is smaller than `Just (x :: xs)` if Idris looks under the matching constructor. The default
value is 5. Increasing the value may slow down totality checking.

``%auto_implicit_depth``
------------------------

Expand Down
5 changes: 5 additions & 0 deletions src/Core/Context.idr
Original file line number Diff line number Diff line change
Expand Up @@ -2252,6 +2252,11 @@ setAmbigLimit : {auto c : Ref Ctxt Defs} ->
Nat -> Core ()
setAmbigLimit max = update Ctxt { options->elabDirectives->ambigLimit := max }

export
setTotalLimit : {auto c : Ref Ctxt Defs} ->
Nat -> Core ()
setTotalLimit max = update Ctxt { options->elabDirectives->totalLimit := max }

export
setAutoImplicitLimit : {auto c : Ref Ctxt Defs} ->
Nat -> Core ()
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Options.idr
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ record ElabDirectives where
ambigLimit : Nat
autoImplicitLimit : Nat
nfThreshold : Nat
totalLimit : Nat
--
-- produce traditional (prefix) record projections,
-- in addition to postfix (dot) projections
Expand Down Expand Up @@ -232,7 +233,7 @@ defaultSession = MkSessionOpts False CoveringOnly False False Chez [] 1000 False

export
defaultElab : ElabDirectives
defaultElab = MkElabDirectives True True CoveringOnly 3 50 25 True
defaultElab = MkElabDirectives True True CoveringOnly 3 50 25 5 True

-- FIXME: This turns out not to be reliably portable, since different systems
-- may have tools with the same name but different required arugments. We
Expand Down
139 changes: 97 additions & 42 deletions src/Core/Termination/CallGraph.idr
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
module Core.Termination.CallGraph

import Core.Case.CaseTree
import Core.Context
import Core.Context.Log
import Core.Env
import Core.Normalise
import Core.Options
import Core.Value

import Libraries.Data.IntMap
import Libraries.Data.SparseMatrix

import Data.String
Expand Down Expand Up @@ -134,56 +137,107 @@ mutual
else pure $ Ref fc Func n
conIfGuarded tm = pure tm

knownOr : SizeChange -> Lazy SizeChange -> SizeChange
knownOr Unknown y = y
knownOr x _ = x
knownOr : Core SizeChange -> Core SizeChange -> Core SizeChange
knownOr x y = case !x of Unknown => y; _ => x

plusLazy : SizeChange -> Lazy SizeChange -> SizeChange
plusLazy Smaller _ = Smaller
plusLazy x y = x |+| y
plusLazy : Core SizeChange -> Core SizeChange -> Core SizeChange
plusLazy x y = case !x of Smaller => pure Smaller; x => pure $ x |+| !y

-- Return whether first argument is structurally smaller than the second.
sizeCompare : Term vars -> -- RHS: term we're checking
sizeCompare : {auto defs : Defs} ->
Nat -> -- backtracking fuel
Term vars -> -- RHS: term we're checking
Term vars -> -- LHS: argument it might be smaller than
SizeChange
Core SizeChange

sizeCompareCon : Term vars -> Term vars -> Bool
sizeCompareConArgs : Term vars -> List (Term vars) -> Bool
sizeCompareApp : Term vars -> Term vars -> SizeChange
sizeCompareCon : {auto defs : Defs} -> Nat -> Term vars -> Term vars -> Core Bool
sizeCompareTyCon : {auto defs : Defs} -> Term vars -> Term vars -> Bool
sizeCompareConArgs : {auto defs : Defs} -> Nat -> Term vars -> List (Term vars) -> Core Bool
sizeCompareApp : {auto defs : Defs} -> Nat -> Term vars -> Term vars -> Core SizeChange

sizeCompare s (Erased _ (Dotted t)) = sizeCompare s t
sizeCompare _ (Erased _ _) = Unknown -- incomparable!
sizeCompare fuel s (Erased _ (Dotted t)) = sizeCompare fuel s t
sizeCompare fuel _ (Erased _ _) = pure Unknown -- incomparable!
-- for an as pattern, it's smaller if it's smaller than either part
sizeCompare s (As _ _ p t)
= knownOr (sizeCompare s p) (sizeCompare s t)
sizeCompare (As _ _ p s) t
= knownOr (sizeCompare p t) (sizeCompare s t)
sizeCompare s t
= if sizeCompareCon s t
then Smaller
else knownOr (sizeCompareApp s t) (if sizeEq s t then Same else Unknown)

sizeCompareCon s t
sizeCompare fuel s (As _ _ p t)
= knownOr (sizeCompare fuel s p) (sizeCompare fuel s t)
sizeCompare fuel (As _ _ p s) t
= knownOr (sizeCompare fuel p t) (sizeCompare fuel s t)
-- if they're both metas, let sizeEq check if they're the same
sizeCompare fuel s@(Meta _ _ _ _) t@(Meta _ _ _ _) = pure (if sizeEq s t then Same else Unknown)
-- otherwise try to expand RHS meta
sizeCompare fuel s@(Meta n _ i args) t = do
Just gdef <- lookupCtxtExact (Resolved i) (gamma defs) | _ => pure Unknown
let (PMDef _ [] (STerm _ tm) _ _) = definition gdef | _ => pure Unknown
tm <- substMeta (embed tm) args zero []
sizeCompare fuel tm t
where
substMeta : {0 drop, vs : _} ->
Term (drop ++ vs) -> List (Term vs) ->
SizeOf drop -> SubstEnv drop vs ->
Core (Term vs)
substMeta (Bind bfc n (Lam _ c e ty) sc) (a :: as) drop env
= substMeta sc as (suc drop) (a :: env)
substMeta (Bind bfc n (Let _ c val ty) sc) as drop env
= substMeta (subst val sc) as drop env
substMeta rhs [] drop env = pure (substs drop env rhs)
substMeta rhs _ _ _ = throw (InternalError ("Badly formed metavar solution \{show n}"))

sizeCompare fuel s t
= if sizeCompareTyCon s t then pure Same
else if !(sizeCompareCon fuel s t)
then pure Smaller
else knownOr (sizeCompareApp fuel s t) (pure $ if sizeEq s t then Same else Unknown)

-- consider two types the same size
sizeCompareTyCon s t =
let (f, args) = getFnArgs t in
let (g, args') = getFnArgs s in
case f of
Ref _ (TyCon _ _) cn => case g of
Ref _ (TyCon _ _) cn' => cn == cn'
_ => False
_ => False
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is not clear to me whether it is sound to make applied type constructor of the same name size-equal regardless of their arguments. But I did not manage to come up with a counter-example (assuming a putative Idris with universe levels/universe polymorphism).

Do you have an intuition why this is correct?

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 think at the time I was considering them irrelevant. I'm asking for things to be the same or smaller when we go under a constructor, and that was my way of ignoring the implicit type arguments.

I think we would have been ok here for now because we don't look under TyCon to determine something is smaller. So if the type got bigger it could still never cause the function to be counted as total.

I've changed it to check the arguments, expecting everything to be the Same or Smaller. It's still contributing Same at best. So, like the status quo, pulling something out from under a TyCon doesn't count as Smaller.

  sizeCompareTyCon fuel s t =
    let (f, args) = getFnArgs t in
    let (g, args') = getFnArgs s in
    case f of
      Ref _ (TyCon _ _) cn => case g of
        Ref _ (TyCon _ _) cn' => if cn == cn'
            then (Unknown /=) <$> sizeCompareProdConArgs fuel args' args
            else pure False
        _ => pure False
      _ => pure False


sizeCompareProdConArgs : {auto defs : Defs} -> Nat -> List (Term vars) -> List (Term vars) -> Core SizeChange
sizeCompareProdConArgs _ [] [] = pure Same
sizeCompareProdConArgs fuel (x :: xs) (y :: ys) =
case !(sizeCompare fuel x y) of
Unknown => pure Unknown
t => (t |*|) <$> sizeCompareProdConArgs fuel xs ys
sizeCompareProdConArgs _ _ _ = pure Unknown

sizeCompareCon fuel s t
= let (f, args) = getFnArgs t in
case f of
Ref _ (DataCon t a) cn => sizeCompareConArgs s args
_ => False

sizeCompareConArgs s [] = False
sizeCompareConArgs s (t :: ts)
= case sizeCompare s t of
Unknown => sizeCompareConArgs s ts
_ => True

sizeCompareApp (App _ f _) t = sizeCompare f t
sizeCompareApp _ t = Unknown

sizeCompareAsserted : Maybe (Term vars) -> Term vars -> SizeChange
sizeCompareAsserted (Just s) t
= case sizeCompare s t of
Ref _ (DataCon t a) cn =>
-- if s is smaller or equal to an arg, then it is smaller than t
if !(sizeCompareConArgs (minus fuel 1) s args) then pure True
else let (g, args') = getFnArgs s in
case (fuel, g) of
(S k, Ref _ (DataCon t' a') cn') => do
-- if s is a matching DataCon, applied to same number of args,
-- no Unknown args, and at least one Smaller
if cn == cn' && length args == length args'
then (Smaller ==) <$> sizeCompareProdConArgs k args' args
else pure False
_ => pure $ False
_ => pure False

sizeCompareConArgs _ s [] = pure False
sizeCompareConArgs fuel s (t :: ts)
= case !(sizeCompare fuel s t) of
Unknown => sizeCompareConArgs fuel s ts
_ => pure True

sizeCompareApp fuel (App _ f _) t = sizeCompare fuel f t
sizeCompareApp _ _ t = pure Unknown

sizeCompareAsserted : {auto defs : Defs} -> Nat -> Maybe (Term vars) -> Term vars -> Core SizeChange
sizeCompareAsserted fuel (Just s) t
= pure $ case !(sizeCompare fuel s t) of
Unknown => Unknown
_ => Smaller
sizeCompareAsserted Nothing _ = Unknown
sizeCompareAsserted _ Nothing _ = pure Unknown

-- if the argument is an 'assert_smaller', return the thing it's smaller than
asserted : Name -> Term vars -> Maybe (Term vars)
Expand All @@ -200,9 +254,10 @@ mutual
mkChange : Defs -> Name ->
(pats : List (Term vars)) ->
(arg : Term vars) ->
List SizeChange
Core (List SizeChange)
mattpolzin marked this conversation as resolved.
Show resolved Hide resolved
mkChange defs aSmaller pats arg
= map (\p => plusLazy (sizeCompareAsserted (asserted aSmaller arg) p) (sizeCompare arg p)) pats
= let fuel = defs.options.elabDirectives.totalLimit
in traverse (\p => plusLazy (sizeCompareAsserted fuel (asserted aSmaller arg) p) (sizeCompare fuel arg p)) pats

-- Given a name of a case function, and a list of the arguments being
-- passed to it, update the pattern list so that it's referring to the LHS
Expand Down Expand Up @@ -310,7 +365,7 @@ mutual
(do scs <- traverse (findSC defs env g pats) args
pure ([MkSCCall fn
(fromListList
(map (mkChange defs aSmaller pats) args))
!(traverse (mkChange defs aSmaller pats) args))
fc]
++ concat scs))

Expand Down
1 change: 1 addition & 0 deletions src/Idris/Desugar.idr
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,7 @@ mutual
PrefixRecordProjections b => do
pure [IPragma fc [] (\nest, env => setPrefixRecordProjections b)]
AmbigDepth n => pure [IPragma fc [] (\nest, env => setAmbigLimit n)]
TotalityDepth n => pure [IPragma fc [] (\next, env => setTotalLimit n)]
AutoImplicitDepth n => pure [IPragma fc [] (\nest, env => setAutoImplicitLimit n)]
NFMetavarThreshold n => pure [IPragma fc [] (\nest, env => setNFThreshold n)]
SearchTimeout n => pure [IPragma fc [] (\nest, env => setSearchTimeout n)]
Expand Down
4 changes: 4 additions & 0 deletions src/Idris/Parser.idr
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,10 @@ directive fname indents
b <- onoff
atEnd indents
pure (PrefixRecordProjections b)
<|> do decoratedPragma fname "totality_depth"
lvl <- decorate fname Keyword $ intLit
atEnd indents
pure (TotalityDepth (fromInteger lvl))
<|> do decoratedPragma fname "ambiguity_depth"
lvl <- decorate fname Keyword $ intLit
atEnd indents
Expand Down
1 change: 1 addition & 0 deletions src/Idris/Syntax.idr
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ mutual
LazyOn : Bool -> Directive
UnboundImplicits : Bool -> Directive
AmbigDepth : Nat -> Directive
TotalityDepth: Nat -> Directive
PairNames : Name -> Name -> Name -> Directive
RewriteName : Name -> Name -> Directive
PrimInteger : Name -> Directive
Expand Down
2 changes: 1 addition & 1 deletion tests/idris2/total/total004/expected
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Main> Main.bar is total
Main> Main.swapR is total
Main> Main.loopy is possibly not terminating due to recursive path Main.loopy
Main> Main.foom is total
Main> Main.pfoom is possibly not terminating due to recursive path Main.pfoom
Main> Main.pfoom is total
Main> Main.even is total
Main> Main.vtrans is possibly not terminating due to recursive path Main.vtrans -> Main.vtrans
Main> Main.GTree is total
Expand Down
5 changes: 5 additions & 0 deletions tests/idris2/total/total025/Issue3317.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
total
f : (a, List a) -> ()
f (_, []) = ()
f (x, _::xs) = f (x, xs)

12 changes: 12 additions & 0 deletions tests/idris2/total/total025/Issue3353.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Data.Fin

covering
g : Fin 64 -> Unit
g FZ = ()
g (FS i') = g $ weaken i'

total
g' : Fin 64 -> Unit
g' FZ = ()
g' i@(FS i') = g' $ assert_smaller i $ weaken i'

14 changes: 14 additions & 0 deletions tests/idris2/total/total025/Pragma.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
%default total

%totality_depth 1

failing "not total"
one : List a -> Nat
one (a :: b :: c :: es) = one (a :: b :: es)
one _ = 0

%totality_depth 3

two : List a -> Nat
two (a :: b :: c :: es) = two (a :: b :: es)
two _ = 0
50 changes: 50 additions & 0 deletions tests/idris2/total/total025/Totality.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
%default total

-- This one needed withHoles on tcOnly (instead of withArgHoles)
-- or the solved implicit for the (ty : Type) on Maybe would not be
-- substituted.
length : Maybe (List a) -> Nat
length Nothing = 0
length (Just []) = 0
length (Just (x :: xs)) = 1 + length (Just xs)

one : List a -> Nat
one (x :: y :: zs) = one (x :: zs)
one _ = 0

two : List a -> Nat
two (a :: b :: c :: d :: es) = two (a :: c :: es)
two _ = 0

three : List a -> Nat
three (a :: b :: c :: d :: es) = three (b :: c :: es)
three _ = 0

failing "not total"
four : List Nat -> Nat
four (a :: b :: c :: es) = four (b :: c :: a :: es)
four _ = 0

-- also needs withHoles
five : (List a, List a) -> List a
five (a :: as, bs) = a :: five (as, bs)
five ([], _) = []

-- This is total, but not supported
failing "not total"
six : (List a, List a) -> List a
six (a :: as, bs) = a :: six (bs, as)
six ([], _) = []


failing "not total"
-- If we didn't check all of the arguments of MkTuple for
-- Same/Smaller, then this would be incorrectly accepted as total
first : (List Nat, List Nat) -> Nat
second : (List Nat, List Nat) -> Nat

first (x :: xs, ys) = second (xs, Z :: ys)
first _ = Z

second (xs, y :: ys) = first (1 :: xs, ys)
second _ = Z
4 changes: 4 additions & 0 deletions tests/idris2/total/total025/expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1/1: Building Issue3317 (Issue3317.idr)
1/1: Building Totality (Totality.idr)
1/1: Building Issue3353 (Issue3353.idr)
1/1: Building Pragma (Pragma.idr)
6 changes: 6 additions & 0 deletions tests/idris2/total/total025/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
. ../../../testutils.sh

idris2 --check Issue3317.idr
idris2 --check Totality.idr
idris2 --check Issue3353.idr
idris2 --check Pragma.idr
2 changes: 1 addition & 1 deletion tests/ttimp/total002/expected
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Yaffle> Main.ack is total
Yaffle> Main.foo is total
Yaffle> Main.foo' is total
Yaffle> Main.foom is total
Yaffle> Main.pfoom is possibly not terminating due to recursive path Main.pfoom
Yaffle> Main.pfoom is total
Yaffle> Bye for now!