Zulip Chat Archive

Stream: lean4

Topic: Breaking up large mutual inductives


Mario Carneiro (Jul 19 2021 at 19:56):

Are there any techniques for dealing with inductive types which contain a large recursion? The issue is that I have an almost acyclic collection of types A ... Z that all build on one another, but A needs to refer to Z, and this causes all the types in between to end up in one giant mutual inductive block, which causes problems for the kernel. I'm considering using unsafe to avoid all the theorem generation because I don't need anything to be verified, I just want long distance mutual recursion like one would find in C / haskell / etc. Lean 4 core has a couple file-spanning mutual recursions, but they generally seem to go via builtin_initialize which isn't available to user code.

Daniel Fabian (Jul 19 2021 at 20:12):

oftentimes, what the F# folks do is being parametric in the type you would have to be recursive on otherwise. Maybe that could work for you?

Mario Carneiro (Jul 19 2021 at 20:14):

You still have to close the loop at the end, though. Is lean able to handle nested inductives like that?

Mac (Jul 19 2021 at 23:20):

I think @Daniel Fabian means something like this:

inductive BaseFoo a
| foo (foobar : a)

inductive BaseBar a
| bar (foobar : a)

inductive Foobar
| foo (foo : BaseFoo Foobar)
| bar (bar : BaseBar Foobar)

abbrev Foo := BaseFoo Foobar
abbrev Bar := BaseBar Foobar

which works like the following mutual block:

mutual

inductive Foo
| foo (foobar : Foobar)

inductive Bar
| bar (foobar : Foobar)

inductive Foobar
| foo (foo : Foo)
| bar (bar : Bar)

end

I've used this pattern myself in the past. For example, I used it to model LLVM's type hierarchy, which you can see here if you want.

Mario Carneiro (Jul 20 2021 at 00:06):

doesn't this incur just as much complexity internally as the mutual block? In order to prove that the nested inductive is okay it has to work out all the intermediate inductives

Mario Carneiro (Jul 20 2021 at 00:07):

it's not so bad here but imagine there are 6 more defs and structures between BaseFoo and the thing that shows up in Foobar

Mac (Jul 20 2021 at 00:18):

Mario Carneiro said:

In order to prove that the nested inductive is okay it has to work out all the intermediate inductives

What do you mean by okay? I don't think any special checking is going on in the non-mutual example. I know that, at the very least, some of the checking that goes on in a mutual block does not occur, since you can mix structures and inductives with this pattern but you can't do so in mutual blocks.

Mario Carneiro (Jul 20 2021 at 00:20):

I think that's just mutual being stingy

Mario Carneiro (Jul 20 2021 at 00:20):

structure is just an inductive under the hood

Mario Carneiro (Jul 20 2021 at 00:22):

there are lots of other things that could be supported in mutual but aren't like variable, set_option, and especially def and structure

Mac (Jul 20 2021 at 00:22):

Mario Carneiro said:

structure is just an inductive under the hood

You've mentioned this before and it has always struck me as odd. As, at the very least, structures support extends which behaves specially in many cases. So, I would not say they are all that identical.

Mario Carneiro (Jul 20 2021 at 00:23):

structures have lots of elaborator magic layered on top but from the kernel's perspective they are just inductives

Mario Carneiro (Jul 20 2021 at 00:24):

they generate all the usual inductive gizmos like noConfusion, plus some more things specific to structures like projections and parent instances

Mac (Jul 20 2021 at 00:24):

Ah, that's it. I tend to think at the Lean level, not the kernel level. For me, that's what matters.

Mario Carneiro (Jul 20 2021 at 00:25):

when asking whether a nested inductive is okay the check is in terms of kernel concepts

Mac (Jul 20 2021 at 00:26):

I am confused though about what part of this you would expect to error. In the non-mutual pattern there isn't really any nested inductives (in the sense that they are mutually referring to one another).

Mario Carneiro (Jul 20 2021 at 00:26):

mutual blocks are part of the elaborator layered on top, which is why it's possible for mutual to reject something that would be perfectly fine if it made it to the kernel

Mario Carneiro (Jul 20 2021 at 00:27):

Foobar is a nested inductive

Mario Carneiro (Jul 20 2021 at 00:28):

in order to validate a nested inductive that has a constructor like mk : Foo T -> T, you have to inspect and unfold Foo down to basic parts, because not every type operator Foo is sound in that position

Mario Carneiro (Jul 20 2021 at 00:28):

specifically, Foo := \lam X, X -> False would cause a contradiction

Mario Carneiro (Jul 20 2021 at 00:29):

The check that has to be done is that Foo unfolds to a nested combination of inductive type operators

Mario Carneiro (Jul 20 2021 at 00:30):

and this process entails essentially building that whole mutual inductive block under the hood

Mario Carneiro (Jul 20 2021 at 00:31):

It might be a bit faster since this construction is happening at a lower level where the elaborator doesn't have to get involved, but from a pure complexity standpoint they should be about the same

Mac (Jul 20 2021 at 00:31):

Ah. Makes sense. Sadly, I don't know much about how the kernel works.

Mario Carneiro (Jul 20 2021 at 00:32):

Plus, the recursor for a nested inductive explicitly references all those intermediates, and lean also wants to prove a bunch of injectivity lemmas about the inductive, and I think this is what costs most of the processing time

Mario Carneiro (Jul 20 2021 at 00:32):

I'm sure you have noticed that if you put unsafe on a large mutual or nested inductive it runs a lot faster

Mac (Jul 20 2021 at 00:33):

That's weird, because I said, I used this pattern to represent LLVM type hierarchy (i.e. here) which had a decent number of different nested inductives and I didn't really notice any overhead. But maybe that wasn't enough nesting?

Mario Carneiro (Jul 20 2021 at 00:49):

you should try playing with the nested inductive here. I gave up trying to fold Command into the big mutual inductive and instead I'm storing the backreferences in an external data structure and storing CommandId in the inductive instead

Mario Carneiro (Jul 20 2021 at 00:51):

It's already enough to exceed maxHeartbeats if you add a few more of those inductives into the mutual block

Mac (Jul 20 2021 at 00:52):

Do you think you could try the parameterized pattern there and see if this might be an elaborator issue with mutual blocks rather than a kernel issue with nested inductives?

Mac (Jul 20 2021 at 00:53):

Because while I haven't experienced slowdowns with the parametrized pattern, I definitely have encountered many a problem with using mutual blocks (including processing time).

Mac (Jul 20 2021 at 00:59):

Though I guess it might be wise to probe the Lean developers further to see if that could really be a solution before engaging in such a large refactor.

Mac (Jul 20 2021 at 01:14):

I do suspect though that the problem may be that mutual blocks have to do a lot more checking because they don't know a priori if the inductives are merely nested or if they are fully mutually recursive.

Mario Carneiro (Jul 20 2021 at 01:25):

Just for fun, here's a plot of the dependency graph between all the inductives (only counting the ones in the SCC) lean3-mutual.png

Daniel Fabian (Jul 20 2021 at 03:39):

Mario Carneiro said:

It might be a bit faster since this construction is happening at a lower level where the elaborator doesn't have to get involved, but from a pure complexity standpoint they should be about the same

have you verified this? I'd expect that you can do the check locally for each parametrized inductive as opposed to having to do it for the whole SCC. The fact that you are parametric breaks the loop after all.

Mario Carneiro (Jul 20 2021 at 03:55):

I actually don't know that much about what it's doing; my paper on the lean kernel was pre-nested inductives and I haven't looked at it since beyond observational testing like this

Daniel Fabian (Jul 20 2021 at 03:56):

as in does adding the type parameter fix your perf problem or not?

Mario Carneiro (Jul 20 2021 at 04:04):

I haven't tried it yet; one type parameter wouldn't be enough to untangle that graph, maybe 3 would get it into a collection of reasonably small mutual inductives but there are little cycles all over

Sebastian Ullrich (Jul 20 2021 at 07:43):

Now I'm glad that Syntax is stringly-typed... it does avoid this whole bunch of issues

Sebastian Ullrich (Jul 20 2021 at 07:44):

Mario Carneiro said:

Plus, the recursor for a nested inductive explicitly references all those intermediates, and lean also wants to prove a bunch of injectivity lemmas about the inductive, and I think this is what costs most of the processing time

You could try set_option genInjectivity false for that

Sebastian Ullrich (Jul 20 2021 at 07:45):

Mario Carneiro said:

but they generally seem to go via builtin_initialize which isn't available to user code.

I think initialize should work there instead

Chris B (Jul 20 2021 at 16:19):

That graph is nicely formatted btw, how'd you make it?

Mario Carneiro (Jul 20 2021 at 17:10):

mathematica, no special settings, just Graph[data, VertexLabels -> All]. I was originally hoping that one of the embeddings would help find good cut points in the graph, but it's pretty interconnected

Mario Carneiro (Jul 20 2021 at 17:13):

I don't suppose anyone knows whether there is an algorithm for finding a permutation of vertices of a directed graph that minimizes the number of backward edges (like topological sort, but for non-dags)

Kyle Miller (Jul 20 2021 at 22:15):

Mario Carneiro said:

I don't suppose anyone knows whether there is an algorithm for finding a permutation of vertices of a directed graph that minimizes the number of backward edges (like topological sort, but for non-dags)

According to section 9.4 of di Battista et al., "Graph Drawing: Algorithms for the Visualization of Graphs," this is an NP-complete problem. They give a greedy linear time algorithm, and they claim it works relatively well for sparse digraphs. (The formulation of the problem is to find the subset of edges to reverse to form a DAG.)

Kyle Miller (Jul 20 2021 at 22:19):

The greedy algorithm calculates a permutation of the vertices (as a list) that tries to minimize the set of leftward edges. Pseudocode:

let L and R be initialized as empty lists.
while there are still vertices:
  remove all sinks, prepending them to the list R
  remove all sources, appending them to the list L
  if G is nonempty,
    choose and remove a vertex maximizing outdeg - indeg, and append it to the list L
the result is L concatenated with R.

Mario Carneiro (Jul 20 2021 at 22:20):

is the vertex chosen in the last step also removed from the graph?

Kyle Miller (Jul 20 2021 at 22:22):

Oh, I made a big mistake translating it. Hold on.

Kyle Miller (Jul 20 2021 at 22:24):

Ok, fixed. I was reading the notation too quickly, sorry.

Marcus Rossel (Jul 22 2021 at 09:35):

Mario Carneiro said:

my paper on the lean kernel

What paper is that?


Last updated: Dec 20 2023 at 11:08 UTC