Stream: t-compiler/wg-polonius

Topic: initialization


Albin Stjerna (Jul 15 2019 at 09:05, on Zulip):

Hi @WG-polonius! By now Niko should be on vacation (DON'T READ NIKO, VACATE!), but I figured I should give working on initialisation a shot anyway. I have a rough idea of what I should do, and some questions, including one on error reporting that overlaps with @lqd's work on illegal subset relations.

Would anyone be willing to have a quick chat about this at some point? It makes more sense to me to see if anyone has answers to my questions before I start digging the answers out of Rust and/or make assumptions.

lqd (Jul 15 2019 at 09:51, on Zulip):

with Niko and Felix gone, maybe Matthew would be a good person to talk to

lqd (Jul 15 2019 at 09:52, on Zulip):

or ofc eddyb

Albin Stjerna (Jul 15 2019 at 10:18, on Zulip):

Hmh, yes probably

Albin Stjerna (Jul 15 2019 at 10:18, on Zulip):

Thanks

Albin Stjerna (Jul 15 2019 at 10:39, on Zulip):

Ping @Matthew Jasper to begin with, would you have the time to talk a bit about initialisation at some point?

Matthew Jasper (Jul 15 2019 at 11:27, on Zulip):

I should be free and at my computer in 7-8 hours. I could answer any really simple questions now.

nikomatsakis (Jul 15 2019 at 11:35, on Zulip):

/me does not read

Albin Stjerna (Jul 15 2019 at 12:16, on Zulip):

@Matthew Jasper thanks! Poke me when you're available, or preferably slightly before that so I can get to a computer :)

Robert Jacobson (Jul 15 2019 at 16:17, on Zulip):

@Albin Stjerna I haven't forgotten your generous offer to read what I write about Polonius, but I got sidetracked by writing a Pratt parser/Pratt parser generator in Rust, a creature that seems to be conspicuously absent from the Rust ecosystem. Just curious, have there been discussions about strengthening the Rust parser ecosystem? Or is it considered not particularly important considering the existence of other well-known external tools like Ragel that already target Rust? I haven't worked in this space long enough to know.

Albin Stjerna (Jul 15 2019 at 17:00, on Zulip):

@Robert Jacobson I don't know, honestly, I'm rather new here myself.

Matthew Jasper (Jul 15 2019 at 17:18, on Zulip):

@Albin Stjerna I should be able to discuss things in 20-30 minutes

Albin Stjerna (Jul 15 2019 at 17:37, on Zulip):

@Matthew Jasper Yay!

Matthew Jasper (Jul 15 2019 at 17:52, on Zulip):

Ok, I'm here

Matthew Jasper (Jul 15 2019 at 17:53, on Zulip):

So, there were some notes on this here: https://paper.dropbox.com/doc/Polonius-and-initialization--Ag9uxQB9rVuyu~JQ71KkYscwAg-mNvR4jqITCdsJDUMEhFbv

Matthew Jasper (Jul 15 2019 at 17:58, on Zulip):

...which I don't have write access to

Matthew Jasper (Jul 15 2019 at 18:02, on Zulip):

But the last section provides some details on what to do.

simulacrum (Jul 15 2019 at 18:02, on Zulip):

If you have an email for me (feel free to PM) I can invite you

Albin Stjerna (Jul 15 2019 at 18:03, on Zulip):

@Matthew Jasper Yes, I had a look at them earlier

Albin Stjerna (Jul 15 2019 at 18:05, on Zulip):

I think I understand the big picture: so I want to produce the following facts:
- "path x1 is a child of path x2"
- "path x was accessed at point p" facts
- "path x was initialised at point p" facts
- "path x was deinitialised at point p"

Albin Stjerna (Jul 15 2019 at 18:06, on Zulip):

I think it would be fairly straightforward to use that to calculate which paths are inintialised maybe/for sure where, and then from that to errors on invalid access

Albin Stjerna (Jul 15 2019 at 18:07, on Zulip):

However, I think I should start by calculating var_initialized_on_exit(V, P) because I already have those tuples collected

Albin Stjerna (Jul 15 2019 at 18:07, on Zulip):

It should be true if the variable V might be initialised at P

Matthew Jasper (Jul 15 2019 at 18:07, on Zulip):

So step 1 is to implement Atom for MovePathIndex (the one defined here: https://github.com/rust-lang/rust/blob/f3f9d6dfd92dfaeb14df891ad27b2531809dd734/src/librustc_mir/dataflow/move_paths/mod.rs#L16)

Albin Stjerna (Jul 15 2019 at 18:07, on Zulip):

Ok, that was another question I had, what I should use for the input

Albin Stjerna (Jul 15 2019 at 18:08, on Zulip):

But that was my best candidate :)

Matthew Jasper (Jul 15 2019 at 18:08, on Zulip):

However, I think I should start by calculating var_initialized_on_exit(V, P) because I already have those tuples collected

That sounds more sensible than trying to implement the remaining half of the borrow checker

Albin Stjerna (Jul 15 2019 at 18:08, on Zulip):

Arr, How Hard Could It Be™

Matthew Jasper (Jul 15 2019 at 18:08, on Zulip):

Very hard with the time you have.

Albin Stjerna (Jul 15 2019 at 18:09, on Zulip):

True

Matthew Jasper (Jul 15 2019 at 18:09, on Zulip):

Most of the facts can be extracted from this struct https://github.com/rust-lang/rust/blob/f3f9d6dfd92dfaeb14df891ad27b2531809dd734/src/librustc_mir/dataflow/move_paths/mod.rs#L99

Albin Stjerna (Jul 15 2019 at 18:09, on Zulip):

Isn't that sort of cheating?

Albin Stjerna (Jul 15 2019 at 18:09, on Zulip):

Because that struct already kind of has all the initialisation data, right?

Matthew Jasper (Jul 15 2019 at 18:10, on Zulip):

It only has where things are initialized and deinitialized, not the initialization flow data.

Albin Stjerna (Jul 15 2019 at 18:11, on Zulip):

Oh, ok, then it makes sense

Albin Stjerna (Jul 15 2019 at 18:11, on Zulip):

Ok, and that one obviously has the mapping from local <-> move paths

Albin Stjerna (Jul 15 2019 at 18:11, on Zulip):

So that was...all my questions I think

Matthew Jasper (Jul 15 2019 at 18:11, on Zulip):

rev_lookup contains the relations between move paths and locals, and the parent relation.

Matthew Jasper (Jul 15 2019 at 18:12, on Zulip):

and moves and initscontain the initializations and deinitializations

Albin Stjerna (Jul 15 2019 at 18:12, on Zulip):

Yes, I'm already using it to get var_initialized_on_exit (which should probably be renamed to var_may_be_initialized_on_exit)

Matthew Jasper (Jul 15 2019 at 18:13, on Zulip):

Which leaves the "used at" facts, which you fortunately don't need for var_may_be_initialized_on_exit

Albin Stjerna (Jul 15 2019 at 18:13, on Zulip):

no, and I already have them right?

Albin Stjerna (Jul 15 2019 at 18:13, on Zulip):

I needed them for liveness

Albin Stjerna (Jul 15 2019 at 18:14, on Zulip):

Unless I need a used at for a move path and not a variable hmm

Matthew Jasper (Jul 15 2019 at 18:14, on Zulip):

Yes

Matthew Jasper (Jul 15 2019 at 18:14, on Zulip):

So liveness considers x.a a use of x, but for move errors it's only a use of x.a

Albin Stjerna (Jul 15 2019 at 18:15, on Zulip):

ah of course

Albin Stjerna (Jul 15 2019 at 18:15, on Zulip):

so it's higher resolution

Matthew Jasper (Jul 15 2019 at 18:16, on Zulip):

Actually, they shouldn't be too bad to generate

Matthew Jasper (Jul 15 2019 at 18:17, on Zulip):

You can add a visit_place method to the implementation here: https://github.com/rust-lang/rust/blob/099f9e4e8aac3968888636e2126c4b7f8e6bb2d3/src/librustc_mir/borrow_check/nll/invalidation.rs#L57

Matthew Jasper (Jul 15 2019 at 18:19, on Zulip):

Then you generate a uses fact if the context is a non-mutating use, or it's a MutatingUse(Borrow).

Albin Stjerna (Jul 15 2019 at 18:20, on Zulip):

Hm, ok, and how do I go from there to the MovePathIndex of the use?

Matthew Jasper (Jul 15 2019 at 18:22, on Zulip):

move_data.rev_lookup.find(), and then extract the move path from the returned value.

Albin Stjerna (Jul 15 2019 at 18:22, on Zulip):

Ah, ok so I have that in the context

Matthew Jasper (Jul 15 2019 at 18:22, on Zulip):

You can add it.

Albin Stjerna (Jul 15 2019 at 18:23, on Zulip):

Ok, that makes sense

Matthew Jasper (Jul 15 2019 at 18:23, on Zulip):

That's not quite correct for Box de-references and inline asm. But it should be a close approximation.

Matthew Jasper (Jul 15 2019 at 18:23, on Zulip):

and it can be fixed.

Albin Stjerna (Jul 15 2019 at 18:24, on Zulip):

Ah, ok

Matthew Jasper (Jul 15 2019 at 18:24, on Zulip):

and assigning to any dereference....

Albin Stjerna (Jul 15 2019 at 18:24, on Zulip):

Haha

Albin Stjerna (Jul 15 2019 at 18:24, on Zulip):

What I have learned from this project is that it's a miracle that any compiler ever works for anything

Matthew Jasper (Jul 15 2019 at 18:25, on Zulip):

So true

Albin Stjerna (Jul 15 2019 at 18:25, on Zulip):

I currently have a sheet of paper containing a very messy mind-map of which module calls which function where to populate data for which other function, and that's just to have an idea for where Polonius intersects with NLL

Albin Stjerna (Jul 15 2019 at 18:26, on Zulip):

Ok, but I think that's more than enough to get me started. Do you also have a suggestion for where to intercept MoveData and start emitting facts?

Albin Stjerna (Jul 15 2019 at 18:28, on Zulip):

Do you think liveness::generate() would do, or should it be earlier?

Matthew Jasper (Jul 15 2019 at 18:32, on Zulip):

I'd probably do it right when we create the AllFacts

Matthew Jasper (Jul 15 2019 at 18:33, on Zulip):

In the totally accurately named nll/mod.rs:compute_regions

Albin Stjerna (Jul 15 2019 at 18:33, on Zulip):

I did wonder about that one

Albin Stjerna (Jul 15 2019 at 18:34, on Zulip):

But that sounds good

Albin Stjerna (Jul 15 2019 at 18:34, on Zulip):

Thank you so much!

Albin Stjerna (Jul 24 2019 at 13:45, on Zulip):

@Matthew Jasper I ran into a snag of sorts; I'm not sure I understand how MIR Locals translate to their corresponding MovePathIndexes. So what I did was I populated the var_starts_path fact from basically dumping the lookups in MovePathLookup so that I have tuples of (Local, MovePathIndex). In my small sample program, this generates one MovePathIndexper Local, basically, which is weird because many of them map to the same place. Basically, I don't know where to look for the information that somehow shows that a case like _1 = _2 means _1 now is the "same" as _2, for some definition of sameness.

Albin Stjerna (Jul 24 2019 at 13:47, on Zulip):

Or, I guess what is weird is that when I try to populate my parentrelation by iterating over all the MovePaths in MoveData and adding a tuple per entry I get from move_path.parents(), I only get one interesting parent relationship and nothing matching the move paths where things are actually moved out of my tuple

Albin Stjerna (Jul 24 2019 at 13:49, on Zulip):

Specifically, my code looks like this:

fn populate_polonius_move_facts(all_facts: &mut AllFacts, move_data: &MoveData<'_>, _location_table: &LocationTable) {
    all_facts.var_starts_path.extend(move_data.rev_lookup.iter_locals_enumerated().map(|(v, &m)| (v, m)));

    for (idx, move_path) in move_data.move_paths.iter_enumerated() {
        all_facts.parent.extend(move_path.parents(&move_data.move_paths).iter().map(|&parent| (parent, idx)));
    }
}

I would have expected the "interesting" MoveIndexes to also have parents, but they don't? Is this because my program isn't interesting enough (i.e. compiles)?

Albin Stjerna (Jul 24 2019 at 13:56, on Zulip):

Perhaps I should hold off judgement until I have Init events and see where that would get me

Albin Stjerna (Jul 24 2019 at 14:43, on Zulip):

Also, when looking at the Initstruct, the documentation says that "initializations can be from an argument or from a statement". What does it mean for an initialisation to happen in an argument?

Matthew Jasper (Jul 24 2019 at 18:28, on Zulip):

Also, when looking at the Initstruct, the documentation says that "initializations can be from an argument or from a statement". What does it mean for an initialisation to happen in an argument?

given fn f(x: i32) {} x is initialized "before the function starts". That's recorded as an initialization from an assignment

Matthew Jasper (Jul 24 2019 at 18:32, on Zulip):

For the other stuff it would be helpful to see the function that you're testing on and output facts

Albin Stjerna (Jul 24 2019 at 20:32, on Zulip):

I'll do some more thinking tomorrow and see if I can figure out what I would even expect it to do and let you know. There is a chance that it actually works and that I'm just confused. I think I should have most of the logic for calculating var_maybe_initialized_on_exit(which should probably be called var_maybe_partially_initialized_on_exit, but that's just too long)

Albin Stjerna (Jul 25 2019 at 10:28, on Zulip):

@Matthew Jasper Ok, so I put everything together and added some compare functionality similar to what I did for provenance variable liveness. I haven't verified that the parenthood relationship works yet, but I have identified a definite problem with terminators that I don't know how to handle, and would very much appreciate your input on.

Situation: there is a basic block, (N) with a terminator terminator(N) that performs one or more function calls with moves (of m) and one assignment (of a), like a = foo(move m), with terminators going to a block (RETURN) and (UNWIND).
What should happen: it's unclear to me whether m should be considered initialised in terminator(N). Does the move always happen before the call? However, it is clear to me that in RETURN, mshould be deinitialised (definitely), and ashould be initialised (definitely).
What actually happens: the rustc-generated var_maybe_initialized_on_exit has:
- var_maybe_initialized_on_exit(a, terminator(N)) = False.
- var_maybe_initialized_on_exit(a, start(RETURN)) = True.
- var_maybe_initialized_on_exit(a, start(UNWIND)) = False.
- var_maybe_initialized_on_exit(m, start(UNWIND)) = False.

However, as Polonius doesn't see the difference between a terminator and a regular point in the program-flow, in other words it treats both arms the same: both the move and the initialisation happens before the unwind arm.

Suggested fix: Would it be sufficient to just emit initialized_at(a, start(RETURN)) in this case, you think?

Matthew Jasper (Jul 25 2019 at 21:38, on Zulip):

Livened is able to ignore most of this due to how MIR is constructed. Moving the initialization to the start of the return block is the correct fix for now. I should probably go through the handling of calls in MIR again sometime, because it's a bit of a hack that doesn't easily port to polonius.

Albin Stjerna (Jul 26 2019 at 14:15, on Zulip):

Ok, I'll try that and see what else breaks/was already broken. :)

Albin Stjerna (Jul 26 2019 at 14:16, on Zulip):

Thanks!

Albin Stjerna (Aug 01 2019 at 12:41, on Zulip):

@Matthew Jasper Ok, I'm almost there now. I just have two problems left:
- my emitted facts and the ones in Polonius don't always agree on when a move caused by a function call happens. This is a bit confusing, as I just emit facts straight from MoveData. I'll have a look at that later, but:
- I currently don't know how to set function arguments as initialised. I know I can find them in MoveData::inits, but they don't have a Locationof any kind in the case of an InitLocation::Argument. So I guess my question is: how do I find the Locationof the start of the function taking a Localas argument?

Albin Stjerna (Aug 01 2019 at 12:42, on Zulip):

(or, I guess I could just find all functions and mark their arguments as initialised if that's easier)

Matthew Jasper (Aug 01 2019 at 13:04, on Zulip):

InitLocation:: Argument really should be Parameter, it should be fine to use the start point of the CFG for them.

Albin Stjerna (Aug 01 2019 at 13:05, on Zulip):

Hmh, and how would I find that?

Albin Stjerna (Aug 01 2019 at 13:06, on Zulip):

I mean, I do have access to the MIR Body, but how do I even tell which function I am in?

Albin Stjerna (Aug 01 2019 at 13:06, on Zulip):

Or is it one Bodyper function?

Albin Stjerna (Aug 01 2019 at 13:07, on Zulip):

Ah yes, ok

Albin Stjerna (Aug 01 2019 at 16:27, on Zulip):

@Matthew Jasper Ok, now the calculated var_maybe_initialized_on_exit:s actually produce the same region_live_at values for all inputs (yay!), and mostly the same input facts as Rust. I still have a problem with moves in function calls though. For example, I have a function like this:

fn main() {
    let x = 13;
    let y = Foo { data: &x };
    println!("y = {:?}", y);
    println!("x = {:?}", x);
}

It has this MIR, and these Polonius inputs/results. Notably, the only difference to the expected values is the status of the moved variables _25 and _46. The discrepancy happens for _25 after the terminator _23 = const std::fmt::ArgumentV1::<'_>::new::<Foo<'_>>(move _24, move _25). In the facts I generate, _25is immediately moved and so is moved on entry to bb2, and on the entire unwind path. In the inputs from rustc (which may be buggy), it is moved by a StorageDead immediately upon entry to 2, but also magically resurrects at the end of the unwind path (in the resume block); despite both versions agreeing that it's uninitialised in both predecessor blocks (3 and 8).

The same thing (except the magical resurrection) happens to _46.

What is the expected behaviour here? Can a move in a terminator somehow not happen? Why is the first argument moved and not the second one?

Matthew Jasper (Aug 01 2019 at 20:23, on Zulip):

I can't get the polonius inputs pdf to load correctly

Matthew Jasper (Aug 01 2019 at 20:25, on Zulip):

_25 and _46 definitely shouldn't be initialized after the function calls.

Albin Stjerna (Aug 02 2019 at 06:51, on Zulip):

Ok, then I’ll have a closer look at the other reported differences and see if they are identical and if so consider it a bug in the current fact generation

Albin Stjerna (Aug 02 2019 at 13:50, on Zulip):

@Matthew Jasper Ok, I have gone through the cases where my output is different from Rustc's, and in almost all cases it's the same problem (a move in a function call not happening until a later StorageDead). In the other two cases it must also be a bug, because they have variables suddenly being initialised without ever having even been even mentioned in the MIR. In other words, I think the computed var_maybe_initialised_on_exitis now correct, with some degree of confidence.

The question now becomes what to do with the old facts. I would like to remove the old var_initialised_on_exitlogic from rustc, and possibly also the entire input fact from Polonius, because it was always an ugly hack. However, that means I can no longer run the comparison tests (some of which fail spuriously anyway).

That also has a follow-on question, which is: should I also throw away the region_live_at input, which I now also have logic to calculate? Similar problems apply here: that would mean that we lose the infrastructure for verifying it, but on the other hand, we already have.

Finally, I could extend the Polonius grammar to also have support for formulating all the initialisation-related facts. This is probably a prerequisite for testing them at all, but it would also be a lot of work for little gain.

What do you think I should do?

Matthew Jasper (Aug 02 2019 at 13:59, on Zulip):

:+1: to removing the licenses and initialized on exit facts.

Albin Stjerna (Aug 02 2019 at 13:59, on Zulip):

"licenses"?

Matthew Jasper (Aug 02 2019 at 14:24, on Zulip):

Liveness

Albin Stjerna (Aug 02 2019 at 14:58, on Zulip):

ah, ok! And what do I do about the tests?

Matthew Jasper (Aug 02 2019 at 18:39, on Zulip):

Remove them

Albin Stjerna (Aug 02 2019 at 19:33, on Zulip):

This is going to be so satisfying :)

Albin Stjerna (Aug 05 2019 at 14:30, on Zulip):

I'm a bit confused about something that should be basic datafrog, but for some reason doesn't work as I expect it to. So I'm fixing a bug in the liveness calculations by filtering out initial values for var_drop_live_atto only contain variables that are actually initialised when they were dropped. I create a static variable for the var_drop_used
relation for use with leapjoins, and then I proceed like this:

// var_drop_live(V, P) :-
//     var_drop_used(V, P),
//     var_maybe_initialzed_on_exit(V, P).
var_drop_live_var.from_leapjoin(
      &var_drop_used_var,
      var_maybe_initialized_on_exit_rel.extend_with(|&(v, _p)| v),
      |&(v, _), &p| (v, p)
  );

Shouldn't this do a join on V, excluding any V:s not in both var_maybe_initialized_on_exit and var_drop_used? Or have I misunderstood how this works? Also, I don't understand why I get wildly different outputs depending on which tuple I select pfrom?

Albin Stjerna (Aug 05 2019 at 14:30, on Zulip):

(Ping @lqd who maybe knows and perhaps doesn't hang around here)

Albin Stjerna (Aug 05 2019 at 14:56, on Zulip):

As far as I can tell, it behaves like an outer join!?

Albin Stjerna (Aug 05 2019 at 19:49, on Zulip):

Update: it's not that, because the same thing happens if I make the "join" myself, but the facts _are_ correct and there should never be any join. I don't understand!? HOW can var_drop_live_at be true for _2 here? It's very clearly not in var_maybe_initialized_on_exit:
pasted image

Albin Stjerna (Aug 05 2019 at 19:50, on Zulip):

(The name "_on_exit" is also wrong, it should be on_entryI guess?)

lqd (Aug 05 2019 at 20:14, on Zulip):

(sorry I'm on holidays and missed the ping)

lqd (Aug 05 2019 at 20:15, on Zulip):

IIUC you're effectively encoding an outer join

lqd (Aug 05 2019 at 20:15, on Zulip):

but the datalog does not only join on V here

lqd (Aug 05 2019 at 20:16, on Zulip):

P is also present in both relations

lqd (Aug 05 2019 at 20:17, on Zulip):

what you're looking for, IIUC, is to filter the (V, P)s present in both relations only

lqd (Aug 05 2019 at 20:18, on Zulip):

however, this won't be possible/easy (it's a gamble depending on the data) with a leapjoin for now

lqd (Aug 05 2019 at 20:19, on Zulip):

they have a weird under-specified/undocumented criterion wrt well-formedness, that is they expect to have a least one extend_with (I think) and even though that's what you have here the leaper for the join should probably be filter_with — but if you do that the leapjoin won't be well-formed anymore (until at least until we relax this requirement in :frog:)

Albin Stjerna (Aug 05 2019 at 20:19, on Zulip):

Haha, I just spent 20 minutes trying to figure out how this happened and was _so proud_ of myself for figuring it out, only to realise you just replied :)

Albin Stjerna (Aug 05 2019 at 20:20, on Zulip):

That was after literal hours of debugging earlier today

lqd (Aug 05 2019 at 20:20, on Zulip):

so, what you're looking for here is a regular join between variables/relations setup to filter, that is, where the whole tuple is a "key" like ((V,P), ()) — there are examples of that in the naive/datafrogopt variants IIRC

lqd (Aug 05 2019 at 20:21, on Zulip):

sorry :)

Albin Stjerna (Aug 05 2019 at 20:21, on Zulip):

Oh that's by no means your fault :)

lqd (Aug 05 2019 at 20:21, on Zulip):

still good that you were able to figure it out on your own :)

Albin Stjerna (Aug 05 2019 at 20:21, on Zulip):

But great, now I think I know how to proceed yay

lqd (Aug 05 2019 at 20:22, on Zulip):

yeah once you know how to encode this pattern you'll be good I think

lqd (Aug 05 2019 at 20:23, on Zulip):

the borrow_live_at here is a good example

lqd (Aug 05 2019 at 20:24, on Zulip):

the join is here

Albin Stjerna (Aug 05 2019 at 20:24, on Zulip):

It looks like literally the same pattern, but with a 3-tuple

Albin Stjerna (Aug 05 2019 at 20:24, on Zulip):

And I also looked at it earlier today when trying to figure out if I did something wrong

Albin Stjerna (Aug 05 2019 at 20:24, on Zulip):

Oh well, I'll go to sleep first I guess

Albin Stjerna (Aug 05 2019 at 20:24, on Zulip):

Thank you! :)

lqd (Aug 05 2019 at 20:25, on Zulip):

the datalog-to-datafrog encoding is a bit hard to read immediately after writing it :)

lqd (Aug 05 2019 at 20:25, on Zulip):

it's .. a bit unreadable for sure

lqd (Aug 05 2019 at 20:26, on Zulip):

you're very welcome, at least I knew how to help here :)

lqd (Aug 05 2019 at 20:35, on Zulip):

(I updated an earlier message to add another case which is even more similar, encoding errors(B, P) :- invalidates(B, P), borrow_live_at(B, P).)

Albin Stjerna (Aug 06 2019 at 07:42, on Zulip):

Ok so now I think I have a different bug in my actual logic haha

Albin Stjerna (Aug 06 2019 at 07:43, on Zulip):

Because I currently ignore all drops of uninitialised variables, and that's not correct is it? At least, that gives different region liveness from the one supplied by rustc

Albin Stjerna (Aug 06 2019 at 07:50, on Zulip):

Ah, I see the bug now. At a drop(), I only consider a variable actually drop-used if it is initialised, but there's an off-by-one error in this logic: it's a tautology that drop(x) means xis not initialised on exit from that statement. I need to join on the previous statement.

Albin Stjerna (Aug 06 2019 at 07:52, on Zulip):

(which will always be just one statement because the drop always occurs mid-point)

Albin Stjerna (Aug 06 2019 at 08:12, on Zulip):

Wohooo that did it!

Albin Stjerna (Aug 06 2019 at 08:12, on Zulip):

And using only joins on relations no less

Albin Stjerna (Aug 06 2019 at 08:33, on Zulip):

In the process of debugging this, I also made a Python script that takes the input from cargo test and produces readable output in Markdown, should I put that somewhere in case it's useful to anyone else?

Albin Stjerna (Aug 06 2019 at 08:35, on Zulip):

It looks something like this:

$ cargo test implicit_fragment | ./parse-diff-output.py
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running target/debug/deps/polonius-55bdf7bb7cdabdf9
error: test failed, to rerun pass '--lib'
## `maybe_initialized_drop_implicit_fragment_drop_main::computed_region_live_at_same_as_input`
- `Start(bb3[0])`:  **missing**: _#13r
- `Mid(bb3[0])`:    **missing**: _#13r
- `Start(bb7[5])`:  **missing**: _#16r
- `Mid(bb7[5])`:    **missing**: _#16r
- `Start(bb8[0])`:  **missing**: _#16r
- `Mid(bb8[0])`:    **missing**: _#16r
- `Start(bb9[0])`:  **missing**: _#16r
- `Mid(bb9[0])`:    **missing**: _#16r
- `Start(bb10[0])`: **missing**: _#16r
- `Mid(bb10[0])`:   **missing**: _#16r
- `Start(bb11[0])`: **missing**: _#16r
- `Mid(bb11[0])`:   **missing**: _#16r
- `Start(bb11[1])`: **missing**: _#16r
- `Mid(bb11[1])`:   **missing**: _#16r
- `Start(bb11[2])`: **missing**: _#16r
- `Mid(bb11[2])`:   **missing**: _#16r
- `Start(bb11[3])`: **missing**: _#16r
- `Mid(bb11[3])`:   **missing**: _#16r
- `Start(bb11[4])`: **missing**: _#16r
- `Mid(bb11[4])`:   **missing**: _#16r
Albin Stjerna (Aug 06 2019 at 08:35, on Zulip):

Hmh, maybe it's not useful anymore now that the big region_live_atcomparisons are soon gone

Albin Stjerna (Aug 07 2019 at 15:57, on Zulip):

@Matthew Jasper cc @lqd

Ok, I have finished generating var_maybe_initialized_on_exit and have removed region_live_at. Last time I checked, it passed all tests, but I'm currently having some troubles in the methods comparing the location-insensitive analysis to the full one. I don't understand them yet, but will debug later.

My implementation doesn't do the full move analysis, but I have fact generation for path access, and I have written comments mocking the Datalog I think is needed. I think it's come to the point where there's a point in having someone take a look at it, if you have the time. There's no rush, because I'm leaving for some vacation until August 15th.

I'm thinking that I should not try to finish the full move analysis as part of my thesis work as Matthew suggested. I will also leave the job of updating polonius-parser to work with these gazillion new facts, but I will absolutely try to do that at some point, just not under deadline.

PRs are at polonius#110 and rust#62800, but they don't have the updated facts yet.

lqd (Aug 07 2019 at 19:48, on Zulip):

nicely done :)

lqd (Aug 07 2019 at 19:49, on Zulip):

don't worry about the parser I can take care of that

lqd (Aug 07 2019 at 19:50, on Zulip):

I think Matthew or Niko would be better than me looking at the rustc side but I can look at the polonius for sure

lqd (Aug 07 2019 at 19:51, on Zulip):

(I'll myself be back from vacation around the same time as you)

Albin Stjerna (Aug 16 2019 at 15:46, on Zulip):

Ok, I have done some further hunting of the bug I mentioned. The problem is this: Polonius + rustc with my latest changes reports no errors for smoke-test's return_ref_to_local(). Or rather, my new logic, including the new region_live_at, works fine. However, sometime after I remove region_live_at, the new fact generation (or Polonius) somehow changes behaviour and it stops working. This is very weird because the problem seems to stem from incorrect subset relation propagation, but there are no changes to either that part of the code, nor to any related fact generation code, and as far as I can tell, the inputs are identical.

Last good commit on my branch is https://github.com/albins/polonius/commit/6b0fc8e6cf0cc5019af26a32898ae8c0064592dd, and the last good one on my Rustc branch is https://github.com/albins/rust/commit/c265a4108307ede999049c71650f95bd2d539d5e.

Albin Stjerna (Aug 16 2019 at 15:47, on Zulip):

I'll continue looking at this tomorrow, but I just can't wrap my head around it!

Albin Stjerna (Aug 16 2019 at 19:07, on Zulip):

Ok, this is nuts. I have the following in the correct output (HEAD of my master branch):
pasted image

and then I have this at HEAD of my working branch:
pasted image

Those are...the same facts?! Also, this is my diff of naive.rs:

diff --git a/polonius-engine/src/output/naive.rs b/polonius-engine/src/output/naive.rs
index 1cf252ed16..028fdfd4d3 100644
--- a/polonius-engine/src/output/naive.rs
+++ b/polonius-engine/src/output/naive.rs
@@ -13,26 +13,37 @@
 use std::collections::{BTreeMap, BTreeSet};
 use std::time::Instant;

+use crate::output::initialization;
 use crate::output::liveness;
 use crate::output::Output;
 use facts::{AllFacts, Atom};

 use datafrog::{Iteration, Relation, RelationLeaper};

-pub(super) fn compute<Region: Atom, Loan: Atom, Point: Atom, Variable: Atom>(
+pub(super) fn compute<Region: Atom, Loan: Atom, Point: Atom, Variable: Atom, MovePath: Atom>(
     dump_enabled: bool,
-    all_facts: AllFacts<Region, Loan, Point, Variable>,
-) -> Output<Region, Loan, Point, Variable> {
+    all_facts: AllFacts<Region, Loan, Point, Variable, MovePath>,
+) -> Output<Region, Loan, Point, Variable, MovePath> {
     let mut result = Output::new(dump_enabled);

+    let var_maybe_initialized_on_exit = initialization::init_var_maybe_initialized_on_exit(
+        all_facts.child,
+        all_facts.path_belongs_to_var,
+        all_facts.initialized_at,
+        all_facts.moved_out_at,
+        all_facts.path_accessed_at,
+        &all_facts.cfg_edge,
+        &mut result,
+    );
+
     let region_live_at = liveness::init_region_live_at(
         all_facts.var_used,
         all_facts.var_drop_used,
         all_facts.var_defined,
         all_facts.var_uses_region,
         all_facts.var_drops_region,
+        var_maybe_initialized_on_exit,
         &all_facts.cfg_edge,
-        all_facts.region_live_at,
         all_facts.universal_region,
         &mut result,
     );
Albin Stjerna (Aug 19 2019 at 10:39, on Zulip):

Update: it's a fact generation bug. The dereferencing of _2never causes a use-fact to be emitted. I think this may be due to the lazy reference generation? I.e. _2 is dereferenced, but only to create a copy, it seems?

What I think should happen if I understand the reasoning correctly is that Polonius should infer through outlives relations that _0 outlives the value it is referring to and return an error, presumably when the value is moved by the StorageDead() just before the terminator, but the propagation of the outlives relation relies on the liveness of _2's provenance variable, r6.

Here is the MIR:

StorageLive(_1)
_1 = const 0i32
FakeRead(ForLet, _1)
StorageLive(_2)
_2 = &_1
_0 = &(*_2)
StorageDead(_1)
StorageDead(_2)

Also, should a used variable be considered used at the start of the expression where it is used, or mid-point?

Albin Stjerna (Aug 19 2019 at 10:42, on Zulip):

As a reminder, I use the following MIR Visitor to extract variable uses:

impl Visitor<'tcx> for UseFactsExtractor<'_> {
    fn visit_local(&mut self, &local: &Local, context: PlaceContext, location: Location) {
        match categorize(context) {
            Some(DefUse::Def) => self.insert_def(local, location),
            Some(DefUse::Use) => self.insert_use(local, location),
            Some(DefUse::Drop) => self.insert_drop_use(local, location),
            _ => (),
        }
    }
}

Clearly there is some type of use I am missing here?

Albin Stjerna (Aug 19 2019 at 10:43, on Zulip):

(I now also use the same Visitor to visit places and emit move-path-uses for future move analysis)

Matthew Jasper (Aug 19 2019 at 11:47, on Zulip):

You're missing a super_place call in your visit_place override.

Albin Stjerna (Aug 19 2019 at 12:14, on Zulip):

What does that do?

Albin Stjerna (Aug 19 2019 at 12:28, on Zulip):

well it...makes the rest work. :)

Albin Stjerna (Aug 19 2019 at 12:28, on Zulip):

Thank you!

Albin Stjerna (Aug 19 2019 at 13:49, on Zulip):

HA, ok @nikomatsakis polonius#110 and rust#62800 are ready for review!

Albin Stjerna (Aug 19 2019 at 13:49, on Zulip):

...presuming it passes the tests

Albin Stjerna (Aug 19 2019 at 13:50, on Zulip):

But it Does On My Machine™

nikomatsakis (Aug 27 2019 at 19:11, on Zulip):

HA, ok nikomatsakis polonius#110 and rust#62800 are ready for review!

just saw this

nikomatsakis (Aug 27 2019 at 19:11, on Zulip):

Great!

Albin Stjerna (Aug 30 2019 at 09:47, on Zulip):

@nikomatsakis Ok, I have been thinking about your comments on var_maybe_initialized_on_exit a lot. I think the case is as follows:

var_drop_live(V, P) :-
     var_drop_used(V, P),
     var_maybe_initialized_on_exit(V, P).
Albin Stjerna (Aug 30 2019 at 09:54, on Zulip):

I also have an idea for how to do move errors:

nikomatsakis (Aug 30 2019 at 11:06, on Zulip):

@Albin Stjerna

That is, whenever a variable had some path initialized, the variable was considered initialized.

Yes, has_any_child_of is saying "if any path that begins with move_path is considered initialized...". That seems to map to the rule for var_maybe_initialized_on_exit for sure.

nikomatsakis (Aug 30 2019 at 11:07, on Zulip):

I'm not sure I fully understand your later comments, let me take a stab

nikomatsakis (Aug 30 2019 at 11:07, on Zulip):

So, just talking about MIR, if you have DROP(x), that is considered a no-op if x has been moved. We don't always know that statically. In a later phase ("elaborate drops"), we will extend the MIR to either eliminate no-op DROP terminators, or to add boolean variables that conditionally execute them if needed.

nikomatsakis (Aug 30 2019 at 11:08, on Zulip):

In some cases they will be transformed into DROP(x.y.z), if only some sub-path is initialized.

nikomatsakis (Aug 30 2019 at 11:09, on Zulip):

(But, in those cases, x and x.y cannot have destructors, due to other NLL rules)

nikomatsakis (Aug 30 2019 at 11:09, on Zulip):

(This all makes sense so far?)

nikomatsakis (Aug 30 2019 at 11:10, on Zulip):

Given how I understand this, though, I don't see why we would care about the initialisation of anything below root-levels of references.

nikomatsakis (Aug 30 2019 at 11:12, on Zulip):

It's true that the current incarnation of NLL is somewhat restrictive about initialized sub-paths. For example you can't compile code like this, iirc:

let x: (_, _);
x.0 = vec![22];
v.1 = vec![44];

but I think we intend to support such things eventually (c.f. https://github.com/rust-lang/rust/issues/54987)

nikomatsakis (Aug 30 2019 at 11:13, on Zulip):

In such a case, you would never have a direct initialization of x

nikomatsakis (Aug 30 2019 at 11:14, on Zulip):

I was imagining then a scenario like this:

let x: (_, _);
x.0 = ...;
std::mem::drop(x.0);
nikomatsakis (Aug 30 2019 at 11:14, on Zulip):

If we only tracked initialization at the level of local variables, we would see that x is initialized after x.0 = ... but we couldn't say for sure that x is not initialized after std::mem::drop, because we only saw a single field moved and not the entire thing.

nikomatsakis (Aug 30 2019 at 11:15, on Zulip):

Anyway, to be clear, I believe that the rules as you wrote them were equivalent to the changes I was suggesting -- but I felt like the setup you had would be a bit less efficient and/or clear.

nikomatsakis (Aug 30 2019 at 11:16, on Zulip):

it seems mildly simpler to say that errors are accessed intersected with maybe_moved?

nikomatsakis (Aug 30 2019 at 11:16, on Zulip):

Ah, I guess you also want an error if it is uninitialized, is your point

nikomatsakis (Aug 30 2019 at 11:16, on Zulip):

Except that uninitialized things are considered moved to start

nikomatsakis (Aug 30 2019 at 11:26, on Zulip):

I see I already wrote a summary of how Lark handled things -- it was doing a different sort of analysis than the one you are doing. In particular, it was just doing the "move errors" part. That is, detecting access to uninitialized or moved paths.

nikomatsakis (Aug 30 2019 at 11:26, on Zulip):

But some parts are relevant.

nikomatsakis (Aug 30 2019 at 11:35, on Zulip):

Re-reading that code, I feel like it too is does more transitive closures than are really necessary

Albin Stjerna (Aug 30 2019 at 11:57, on Zulip):

Anyway, to be clear, I believe that the rules as you wrote them were equivalent to the changes I was suggesting -- but I felt like the setup you had would be a bit less efficient and/or clear.

I guess the key question here is: would that lead to correct calculations of region_live_at?

Albin Stjerna (Aug 30 2019 at 11:58, on Zulip):

Except that uninitialized things are considered moved to start

Avoiding a notion of "to start" is specifically why I went about the acrobatics I described; I don't have any concept of "to start" in Datafrog, right?

Albin Stjerna (Aug 30 2019 at 11:58, on Zulip):

There is no first_node_of_cfg, and I'd rather not just guess it's index 0

nikomatsakis (Sep 03 2019 at 19:58, on Zulip):

so I landed https://github.com/rust-lang/polonius/pull/110

nikomatsakis (Sep 03 2019 at 19:58, on Zulip):

now we need a new polonius release, right?

nikomatsakis (Sep 03 2019 at 19:58, on Zulip):

(am I the only one with such privileges?)

Albin Stjerna (Sep 03 2019 at 19:58, on Zulip):

Yes please

Albin Stjerna (Sep 03 2019 at 19:58, on Zulip):

and yes, I think so

Albin Stjerna (Sep 03 2019 at 19:59, on Zulip):

/me has no idea

nikomatsakis (Sep 03 2019 at 20:01, on Zulip):

hmm no it should be more folks

nikomatsakis (Sep 03 2019 at 20:01, on Zulip):

I think @lqd can do it, for example

lqd (Sep 03 2019 at 20:01, on Zulip):

can I ?!

lqd (Sep 03 2019 at 20:02, on Zulip):

I didn't know that

nikomatsakis (Sep 03 2019 at 20:02, on Zulip):

maybe :)

nikomatsakis (Sep 03 2019 at 20:03, on Zulip):

the wg-compiler-nll github team has access, which I might actually want to revoke, along with the compiler team

nikomatsakis (Sep 03 2019 at 20:03, on Zulip):

but I think I should add the compiler-contributors team

nikomatsakis (Sep 03 2019 at 20:03, on Zulip):

which includes you

nikomatsakis (Sep 03 2019 at 20:03, on Zulip):

you're probably also a member of wg-compiler-nll ...

lqd (Sep 03 2019 at 20:04, on Zulip):

yes

nikomatsakis (Sep 03 2019 at 20:04, on Zulip):

anyway I'll publish v0.10.0

nikomatsakis (Sep 03 2019 at 20:07, on Zulip):

done -- @Albin Stjerna do you want to modify rust#62800 to use it?

lqd (Sep 03 2019 at 20:09, on Zulip):

(thanks for the release, I'll learn how to do it in the future so we don't have to bother you :)

nikomatsakis (Sep 03 2019 at 20:16, on Zulip):

no worries, I just don't like things to be bottlenecked on me

Albin Stjerna (Sep 03 2019 at 21:06, on Zulip):

I'll do it tomorrow!

Albin Stjerna (Sep 03 2019 at 21:06, on Zulip):

Signed, going to bed

Albin Stjerna (Sep 04 2019 at 07:53, on Zulip):

@nikomatsakis rust#62800 should be ready to merge once CI passes!

lqd (Sep 04 2019 at 08:33, on Zulip):

@Albin Stjerna ah I see some tidy checks failed, CI didn't get to run yet

Albin Stjerna (Sep 04 2019 at 08:35, on Zulip):

sigh of course

Albin Stjerna (Sep 04 2019 at 08:35, on Zulip):

Guess it must be due to the rebase or something, I’ll have a look

Albin Stjerna (Sep 04 2019 at 08:52, on Zulip):

Ah, it was the file where the try blocks cause rustfmt to error out, that explains things

Albin Stjerna (Sep 04 2019 at 08:53, on Zulip):

Anyway, it passes now

Albin Stjerna (Sep 17 2019 at 08:00, on Zulip):

@lqd By the way, I plan on eventually following Niko's suggestions and perform a precise analysis on move paths. You mentioned that I could use the rustc unit tests. Do you...err...know how to run those with Polonius? Also, that should probably go in the book at some point, I guess

lqd (Sep 17 2019 at 08:23, on Zulip):

yeah it's the --compare-mode polonius flag to add to your x.py test invocation

lqd (Sep 17 2019 at 08:25, on Zulip):

note that right now there are 2 OOMS, and 4-5 benign failures tho

lqd (Sep 17 2019 at 08:26, on Zulip):

since they're in the run pass maybe --pass check could be helpful in avoiding the OOMs, but I didn't try that (as I had them deleted locally to try to compare durations)

Albin Stjerna (Sep 17 2019 at 12:58, on Zulip):

@lqd Thanks, I guess I can just ignore those. Do you know how comprehensive those tests are, otherwise? I.e. how likely are they to catch a subtle mistake?

lqd (Sep 17 2019 at 13:00, on Zulip):

I guess subtlety can be easily missed and it can depend on the specific domain you're testing

lqd (Sep 17 2019 at 13:00, on Zulip):

different areas are more or less well covered

lqd (Sep 17 2019 at 13:01, on Zulip):

say, it'll be better to pass those 10K tests than none at all :)

lqd (Sep 17 2019 at 13:02, on Zulip):

and if the area isn't covered well enough, we can always, and should, add more tests

lqd (Sep 17 2019 at 13:03, on Zulip):

in general there are some tests in many areas

Albin Stjerna (Oct 14 2019 at 15:18, on Zulip):

@lqd You mentioned a problem with leapjoins that only filtered, does this happen when you have an expression like:

path_definitely_initialized_at(Path, Point) :-
        path_maybe_initialized_on_exit(Path, Point),
        !path_maybe_moved_at(Path, Point).

Where all components are actually Relations?

lqd (Oct 14 2019 at 15:19, on Zulip):

if you're talking about Relation::from_leapjoin or similar I don't know; I only used them with variables

lqd (Oct 14 2019 at 15:20, on Zulip):

I assume it could apply

Albin Stjerna (Oct 14 2019 at 15:20, on Zulip):

Ah, ok. But is it a compilation error or a runtime error?

lqd (Oct 14 2019 at 15:20, on Zulip):

the WF-ness rule is : "there needs to be an extend_with" (IIRC, surely Frank would know better, but I think it's this)

Albin Stjerna (Oct 14 2019 at 15:20, on Zulip):

Ah, ok

lqd (Oct 14 2019 at 15:20, on Zulip):

runtime error, sometimes

lqd (Oct 14 2019 at 15:20, on Zulip):

it depends on the data IIRC

Albin Stjerna (Oct 14 2019 at 15:20, on Zulip):

Is there an easy work-around?

lqd (Oct 14 2019 at 15:21, on Zulip):

for variables yes, use a regular join hehe

lqd (Oct 14 2019 at 15:21, on Zulip):

for relations I don't know sorry, niko added that API but I've never had the opportunity to use it yet

Albin Stjerna (Oct 14 2019 at 15:21, on Zulip):

I'll try it and see what happens

Albin Stjerna (Oct 14 2019 at 15:22, on Zulip):

My original problem was that I wanted to antijoin on a variable, which you cannot do (I guess because it might lead to future tuples needing to be retracted)

Albin Stjerna (Oct 14 2019 at 15:22, on Zulip):

So I had to stage my computation

Albin Stjerna (Oct 14 2019 at 15:22, on Zulip):

Which means that in the end, all I really have are Relations

lqd (Oct 14 2019 at 15:23, on Zulip):

not even one variable ?

Albin Stjerna (Oct 14 2019 at 15:23, on Zulip):

Nope

lqd (Oct 14 2019 at 15:23, on Zulip):

maybe you can then artificially make one variable

Albin Stjerna (Oct 14 2019 at 15:23, on Zulip):

I guess I can

Albin Stjerna (Oct 14 2019 at 15:24, on Zulip):

It works as a quick work-around :)

lqd (Oct 14 2019 at 15:24, on Zulip):

what relation are you producing ?

Albin Stjerna (Oct 14 2019 at 15:25, on Zulip):

So it's a four-stage process: first I compute transitive closure of path accesses, then I use those to compute paths that may be moved or not moved, then I calculate the definitely-moved ones and I use those to generate move errors

Albin Stjerna (Oct 14 2019 at 15:25, on Zulip):

Basically, move errors = path access - known initialised paths

lqd (Oct 14 2019 at 15:25, on Zulip):

seems like move errors are an intensional predicate

Albin Stjerna (Oct 14 2019 at 15:26, on Zulip):

known initialised paths = maybe initialised paths - maybe moved paths

Albin Stjerna (Oct 14 2019 at 15:26, on Zulip):

err maybe?

Albin Stjerna (Oct 14 2019 at 15:26, on Zulip):

(what does that mean?)

lqd (Oct 14 2019 at 15:27, on Zulip):

it's producing tuples, not input data; those are usually variables, except for your stratification problem as you mentioned, there you had no choice

Albin Stjerna (Oct 14 2019 at 15:27, on Zulip):

Ah, ok, yes

lqd (Oct 14 2019 at 15:29, on Zulip):

but yeah if stratification is turning every join into a separate iteration, that's unfortunate

lqd (Oct 14 2019 at 15:29, on Zulip):

but yeah a couple variables at the last step should unblock you

Albin Stjerna (Oct 14 2019 at 15:31, on Zulip):

Yes, thanks!

Albin Stjerna (Oct 14 2019 at 16:07, on Zulip):

Ok, revamped initialisation logic compiling now :)

Albin Stjerna (Oct 14 2019 at 16:07, on Zulip):

Let's see if it messed up any ui-tests first, and then worry about how to verify move errors

Albin Stjerna (Oct 14 2019 at 19:11, on Zulip):

This looks...slightly promising? I can't see how these failures would be due to initialisation:

failures:
    [ui] ui/abi/cross-crate/anon-extern-mod-cross-crate-2.rs
    [ui] ui/abi/duplicated-external-mods.rs
    [ui] ui/abi/extern/extern-crosscrate.rs
    [ui] ui/abi/foreign/foreign-dupe.rs
    [ui] ui/abi/invoke-external-foreign.rs
Albin Stjerna (Oct 14 2019 at 19:12, on Zulip):

All the errors are from ld

Albin Stjerna (Oct 15 2019 at 11:17, on Zulip):

Hmm, difficult to say if I changed anything: literally stopping all generation of child in Rust gives me no additional errors, so either I'm doing something wrong or it doesn't cover move paths x drop-liveness very much

lqd (Oct 15 2019 at 11:35, on Zulip):

what kind of errors are you expecting and where ?

Albin Stjerna (Oct 15 2019 at 12:42, on Zulip):

That is a good question. So what I did was I stopped emitting child at all, which would mean that nothing would be drop-live, so I would expect Polonius to under-report some errors with structs implementing their own drop if they hold a loan that is violated.

Albin Stjerna (Oct 15 2019 at 12:43, on Zulip):

I know I lifted some examples from the compiletests into Polonius inputs, and those should at least cause problems

Albin Stjerna (Oct 15 2019 at 12:44, on Zulip):

...ok no I get it it shouldn't cause a problem

Albin Stjerna (Oct 15 2019 at 12:44, on Zulip):

...because initialisation is always precise enough I think, or at least it is in this case

Albin Stjerna (Oct 15 2019 at 12:56, on Zulip):

Anyway, the shared context stuff would have been nice now, because then I could use it to report errors from the initialisation step :)

Last update: Nov 15 2019 at 20:05UTC