Ginko-X / Streaming_NESL

1 stars 0 forks source link

Proof of lemma 0.3 #26

Closed Ginko-X closed 6 years ago

Ginko-X commented 7 years ago

I still got some problems with the proof the important Lemma 0.3 (changed as lemma 1.1 in the pdf). Here is my analysis and current solution, but not sure if it is correct. concatlemma.pdf

afilinski commented 7 years ago

First, it's a little hard for me to read the note because you have apparently updated the definitions of ~^S and |><|^S, but they are not included in the pages you uploaded, nor is there an updated version of the whole document in the main GitHub tree. But I'll assume that they are more or less like what we talked about last week.

Second, Lemma 1.1 actually doesn't hold as stated even in the simple case where p is just a single transducer invocation and S is empty. The reason is the second rule in the transducer semantics (it would be nice if all the rules had names), which says that, when the control stream is empty, the output stream is always taken to be empty, even if there is still some (garbage) data in one or more of the input streams to be read. For example, with c = <()>, a1 = <2>, and a2 = <3,4>, we get MapTwo+(a1,a2) ↓^c <5>, so the extra <4> in the second stream is ignored. But if we concatenate two copies of the inputs, i.e, invoke "MapTwo+(a1++a1,a2++a2) ↓^(c++c) ?", we get MapTwo_+(<2,2>,<3,4,3,4>) ↓^<(),()> <5,6> instead of the <5,5> that the concatenation lemma would claim. (You should have discovered that when doing the proof for that case, if you did it carefully.)

The simplest fix is probably to require, in the second rule, that all the input streams are empty whenever the control stream is empty. Then the above situation cannot arise, because there cannot exist a derivation of "MapTwo_+(<2>,<3,4>) ↓^<()> <5>" in the first place. Note that this requirement corresponds to having an explicit runtime check in the interpreter to verify that the transducer has fully consumed all its input streams, once it reaches the end of the control stream. It may be possible to prove later that all SVcode generated from a SNesl program will be sufficiently well behaved that this check isn't strictly necessary, but then you'll need to generate specific extra code that checks for length mismatches in zip-like comprehensions, e.g. when compiling "{x+y : x in {2}, y in {3,4}}". (There's actually a separate issue in proving that if evaluation of a SNesl expression fails with a runtime error, such as mismatching lengths in "zip", applying "the" to a non-singleton stream, applying "iota" to a negative argument, etc., then the compiled SVcode will also fail, rather than return a random result; but let's not worry about that right now.)

Now back to the case for WithCtrl. Here we have the problem that when the new control stream is empty, the body code is not executed at all, thus bypassing any runtime checks within it. In other words, the semantics of WithCtrl with a special case for an empty control stream (which is only really needed to preserve the step cost, and to avoid infinite unfolding in recursive definitions, neither of which is an issue for Level 0), may differ from the naive semantics that uniformly executes the WithCtrl body, and for which it should be (relatively; see below) straightforward to prove the concatenation lemma.

I think the simplest solution will be to also add a runtime check to WithCtrl, i.e., in the rule for WithCtrl(s,p) where sigma(s) = <>, there should be an additional requirement in the side condition that all the free variables of p are also bound to empty streams in sigma. That will ensure that prepending or appending them to streams from the other derivation will have no effect, so that you can just reuse that derivation directly. (You'll probably need a separate lemma saying something like, if two sigmas agree on all the free variables of a program p, then executing p in them will also yield output stores that agree on all thew new streams added by p.) Again, it may be possible to prove later that this runtime check can safely be omitted, which would simplify the streaming interpreter a bit.

Note also that, in the case for concatenating two non-empty executions of WithCtrl, you will probably need to adjust the set S when invoking the IH on the body program. It may be relevant to already now only consider programs where every WithCtrl has been explicitly annotated with an import list (which should be a conservative over-approximation of the actual set of free variables of the WithCtrl body). That could make it easier to keep track of what S should be at each point in the proof; and possibly the lemma only actually holds for well-annotated programs (which should include all the ones generated by the compiler), rather than all syntactically well-formed ones.

As for your specific questions, maybe incorporating the above changes will be enough, but in case not:

Q1: It is true that the current semantics ensures that a program will never be executed with an empty control stream. However, I don't think it should be necessary to formalize that invariant in either the semantics or the proofs, as it probably wouldn't simplify anything. And it may actually be nice to leave the possibility open to also have a "quick" variant of the WithCtrl instruction, that doesn't check for emptiness at runtime (which is a fairly involved process in the streaming interpreter), but always executes the body, even if the new control stream happens to be empty. This could be used, e.g., for the WithCtrl in iota, and possibly also the one in pack, where it might end up costing more in practice to detect and handle packing of already empty streams as a special case.

Q2: I'm not sure if you are simply proposing a change of notation (which would have no effect on the actual semantics or proofs), or something deeper. In the former case, however, I would say that the current notation is preferable, because otherwise in some contexts psi will have k arguments and in others it will have k+1, which is likely to lead to confusion.

Q3: I don't think EmptyCtrl should present any new problems that are not already introduced by iota(0), i.e., essentially by generating an empty control stream with Usum(<T>). So if everything works out for iota, it shouldn't be necessary to worry separately about EmptyCtrl.

Ginko-X commented 6 years ago

If we fix the 2nd rule of transducer semantics P-X-Termi as \psi(<>,...,<>)↓^<> <>, and add the side condition forall s in Sin. sigma(s) = <> to the evaluation rule P-Wc-Emp, then it seems possible to prove this lemma.

In the code, we have rinx only to check that when the control stream has not reached EOS the input streams cannot be EOS either. But we don't have a runtime check to verify the situation you mentioned here. And then you also pointed out:

It may be possible to prove later that all SVcode generated from a SNesl program will be sufficiently well behaved that this check isn't strictly necessary, ... (There's actually a separate issue in proving that if evaluation of a SNesl expression fails with a runtime error, ... then the compiled SVcode will also fail...)

So it's a little bit confusing what we need to do or not.

Ginko-X commented 6 years ago

I have added the proof of this lemma in the pdf. Please let me know if there is some problem. Also, if necessary, we can meet later today? Or I think I can start Level-1 with Packs and Distrs?

afilinski commented 6 years ago

This looks much better now. There are still a couple of things to fix, but I don't think it should be necessary to meet about them today:

(There are a few other, minor presentational/notational issues, but those can easily wait to our next meeting.)

Assuming all of the above works out, I think you can indeed move on to supporting "using", I think in the first instance, it'll be enough to just consider Distr'ing int-typed using-variables, which is all that is allowed in general comprehensions. Once that's properly proved, I don't think it'll be particularly hard to extend to restricted comprehensions and Pack'ing; and so I think it may be better to start looking at function definitions and calls instead, to make sure there are no surprises there.

Ginko-X commented 6 years ago

Most of these make sense. One small thing (but requires many small changes), about the 4th point: We now have changed SVCODE syntax like this: image

This represents a program as a flat list of instructions, but I feel not very comfortable with this as it is not as close to our code representation as the first version (with four cases of p) does. For example, now we should formally represent a high-level const 3 in SVCODE as S1 := Const_3(); ε instread of just S1 := Const_3() ?

So if this syntax change is not essential, I would prefer to still use the first version syntax, which handles the sequential instructions separately and gives shorter evaluation rules for P-Xducer, P-Wc-Emp, P-Wc-Nonemp.

afilinski commented 6 years ago

I think both approaches will ultimately work; it's just a matter of moving the work around: for each approach, some things will be easier, while others will require slightly more complicated proofs. The main concern is to be consistent, i.e., to pick one definition and stick with it.

That said, I think having arbitrarily tree-shaped sequences of Svcode instructions is both a bit unnatural for a low-level language, and also differs somewhat from what actually happens in the compiler. Note also that it leads to a more involved definition of fv(p1;p2) that also involves dv(), and thus might complicate the proofs for everything involving sequencing.

Maybe a nice middle ground will be to have a separate grammar for single instructions/commands (SDefs, WithCtrls, and later also SCalls), with a corresponding operational-semantics judgement, and also a separate grammar for programs, which would be just flat lists of instructions to be executed sequentially, exactly as in the actual code. That will avoid cluttering all the semantics rules for individual instructions with extra premises for executing the rest of the program, since that will be handled once and for all in the judgment for programs. And concatenating lists of instructions and reasoning about such concatenations should also be quite simple and uniform.