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

Add namespace support for rename operation #12563

Closed
wants to merge 1 commit into from

Conversation

edwardw
Copy link
Contributor

@edwardw edwardw commented Feb 26, 2014

Maintains two renaming tables, one for regular idents and one for
lifetime idents (a loop label is of lifetime kind) so that let bindings
and loop labels with same name won't clash. Also, the mtwt hygiene code
is now @-free.

Closes #12512.

@huonw
Copy link
Member

huonw commented Feb 26, 2014

@jbclements: feedback?

@edwardw
Copy link
Contributor Author

edwardw commented Feb 26, 2014

Be warned that this change comes with cost. Just tried #10607 out:

$ echo 'fn main() { ' 'let _x: int; /*'{1..5000}'*/' '}'  |  rustc - -Z time-passes
...
time: 7.136 s   expansion
...
time: 0.031 s   resolution

$ echo 'fn main() { ' 'let _x: int; /*'{1..5000}'*/' '}'  |  /patched/rustc - -Z time-passes
...
time: 17.769 s  expansion
...
time: 10.569 s  resolution

@alexcrichton
Copy link
Member

I'm excited to see work done in this area, thank you!

That performance regression seems quite worrisome, however, do you know why it's happening?

@huonw
Copy link
Member

huonw commented Feb 27, 2014

(I'm a strong r- on this with the performance regressions, in particular because I think #12512 (comment) would work without a huge hit, even if it's not quite as clean. FWIW, ast::visit actually does a similar thing with visit_fn.)

However, without examining the code yet, I imagine you just may have accidentally added a source of O(n^2) (or worse) behaviour.

Some(ts) => ts.clone()
};
tables.borrow().with_mut(|ts|
ts.find_or_insert_with(ns, |_| Rc::new(RefCell::new(new_sctable_internal())))
Copy link
Member

Choose a reason for hiding this comment

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

I don't quite understand why we need a hashmap of two things.

struct SCTables {
     lifetime: Rc<RefCell<SCTable>>
     plain: Rc<RefCell<SCTable>>
}

should work. (Stored as just SCTables in TLD.)

(I imagine this is most of the performance problem: hashing 1 bits has a lot of overhead.)

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'll experiment with this. I was anticipating the possible (now seems to be highly unlikely) event of adding a third IdentKind so that SCTables must keep synchronised. With hashmap it'll do so automatically. But of course you are right; the performance penalty is just too much.

Copy link
Member

Choose a reason for hiding this comment

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

If you select the field with a match like

match ns {
    PlainIdent => ...,
    LifetimeIdent => ...,
}

then the addition of a new IdentKind will cause compilation to fail unless these are updated too (not quite automatic, but not a risk of a bug).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Stupid me :)

@edwardw
Copy link
Contributor Author

edwardw commented Feb 27, 2014

Turns out in the middle of recursion, I replaced f(get_a(x)) with x.with_a(f). My wild guess is somehow f is not at the tail position any more and LLVM is prevented from TCO. If such speculation has any merit, then we are talking about potential O(2n). How often does that happen? I'm amazed that rust can compile itself at all without blowing up the stack. :D

Also @huonw, I did take your advice #12512 (comment) and added an additional argument to fold_ident. It's just harder to do the same at fold_expr level since all fold_tts knows is identifier.

$ echo 'fn main() { ' 'let _x: int; /*'{1..5000}'*/' '}'  |  /patched/rustc - -Z time-passes
...
time: 6.803 s   expansion
...
time: 0.029 s   resolution

r? @huonw @alexcrichton

@huonw
Copy link
Member

huonw commented Feb 27, 2014

Turns out in the middle of recursion, I replaced f(get_a(x)) with x.with_a(f). My wild guess is somehow f is not at the tail position any more and LLVM is prevented from TCO

Does this really change it significantly? (If you feel like it, it might be interesting to revert back to the with_a call to see if it's actually making a difference.)

It's just harder to do the same at fold_expr level since all fold_tts knows is identifier.

I don't understand what you mean by this sentence.

(Also, it looks like fold needs more LifetimeIdents: e.g. fn foo<'a>(...) should be folding that a as a lifetime.)

@edwardw
Copy link
Contributor Author

edwardw commented Feb 28, 2014

Does this really change it significantly? (If you feel like it, it might be interesting to revert back to the with_a call to see if it's actually making a difference.)

Yes, it did. It was the offender and surprisingly, HashMap doesn't hurt very much.

It's just harder to do the same at fold_expr level since all fold_tts knows is identifier.

I meant fold_tts can't make use of fold_expr. If wen don't deal with the rename namespace at the fold_ident level, the very leaf, then other folder implementations may be forced to always think about expr, tts and ident too.

At the moment, I don't handle the 'real' lifetime folding at all, just the loop labels.

@@ -651,248 +649,296 @@ pub fn pat_is_ident(pat: @ast::Pat) -> bool {
}

// HYGIENE FUNCTIONS
pub mod mtwt {
Copy link
Member

Choose a reason for hiding this comment

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

I feel like this could move to be either syntax::mtwt or syntax::ext::mtwt rather than squirrelled away in this rather mix-and-match "util" module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe syntax::mtwt, syntax::ext are mostly macro definitions.

Copy link
Member

Choose a reason for hiding this comment

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

Well... the whole expansion and hygiene renaming phases happen in syntax::ext.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, syntax::ext it is.

Copy link
Member

Choose a reason for hiding this comment

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

Also, while you're at it, adding a doc comment along the lines of

//! Machinery for hygienic macros, as described in the MTWT[1] paper.
//!
//! [1] Matthew Flatt, Ryan Culpepper, David Darais, and Robert Bruce Findler. 2012. *Macros that work together: Compile-time bindings, partial expansion, and definition contexts*. J. Funct. Program. 22, 2 (March 2012), 181-216. DOI=10.1017/S0956796812000093 http://dx.doi.org/10.1017/S0956796812000093 

(with more line breaks... ;) )

@huonw
Copy link
Member

huonw commented Feb 28, 2014

This seems very reasonable to me (the idea as well as the implementation), but I'd like to just get some confirmation from @jbclements that this isn't going to fundamentally break our macros (or anyone else reasonably familiar with it).

@edwardw
Copy link
Contributor Author

edwardw commented Feb 28, 2014

The mtwt hygiene code moved to syntax::ext::mtwt and all comments, I believe, addressed. There's even a bonus:

$ echo 'fn main() { ' 'let _x: int; /*'{1..5000}'*/' '}' | rustc - -Z time-passes
...
time: 6.745 s   expansion
$ echo 'fn main() { ' 'let _x: int; /*'{1..5000}'*/' '}' | /patched/rustc - -Z time-passes
...
time: 3.723 s   expansion

It could come from other (unrelated) patches I pulled in, though :D Seriously, it should have something to do with fixing FIXME 5074, which improves mtwt::new_rename and mtwt::new_mark.

table: &SCTable,
resolve_table: &mut ResolveTable) -> Name {
let key = (id.name, id.ctxt);
match resolve_table.contains_key(&key) {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if there would be some speed gains in resolve by writing this as

match resolve_table.find(&key) {
    Some(&name) => return name,
    None => {}
}

let resolved = { ... };
...

(to only hash key once; I have a strong feeling that hashing once rather than twice was a major contributor to the speed-up seen on the #10607 smoke-test.)

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'm working on it. It is exclusively mutably borrowed and recursive so the find_or_insert_with trick doesn't seem to work right away.

Copy link
Member

Choose a reason for hiding this comment

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

I think the early return is as good as you'll do here. :) (And it's certainly the lowest hanging fruit.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The resolve time was dozen milliseconds so -Z time-passes is not able to properly gauge speed gains here, not statistically convincingly at least. Anyway, I gonna check this in; the code is much better that way.

@huonw
Copy link
Member

huonw commented Mar 1, 2014

Awesome work on the last commit! (Code that's shorter, simpler and faster!)

@edwardw
Copy link
Contributor Author

edwardw commented Mar 1, 2014

Another 2x speed gain by specializing hasher for mtwt tables.

@huonw
Copy link
Member

huonw commented Mar 1, 2014

(This needs a rebase too.)

@flaper87
Copy link
Contributor

flaper87 commented Mar 3, 2014

great work here! 👍

@edwardw
Copy link
Contributor Author

edwardw commented Mar 3, 2014

Rebased. There was a massive change from ~[T] to Vec<T>, which made this round of rebasing rather painful. The performance also suffered a bit because of it.

@huonw
Copy link
Member

huonw commented Mar 3, 2014

The performance also suffered a bit because of it.

I'm... surprised by this: theoretically Vec is faster... without looking at the code again, the only thing I can think of is #11751.

fn new_mark_internal(m: Mrk, tail: SyntaxContext, table: &SCTable) -> SyntaxContext {
let key = MarkKey { mrk: m, tail: tail };
let mut mark_memo = table.mark_memo.borrow_mut();
let new_ctxt = || idx_push(table.table.borrow_mut().get(), Mark(key));
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, why is there two layers of closures here? Couldn't this just be inlined in to the |_| ...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

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 have to say:

let new_ctxt = |_: &(SyntaxContext, Mrk)| ...
*mark_memo.get().find_or_insert_with(key, new_ctxt)

The type inference get choked here :)

@huonw
Copy link
Member

huonw commented Mar 3, 2014

Ok, looked over it; there's nothing obvious about why it would be particularly slower.

Also, I thought about it a bit, and I'm not so keen on the custom hasher: it does give good speed-ups, but it might be better to wait until the centralised hasher has been optimised some more before we start overriding default type params and generally spreading Hasher impls through out the codebase (#12635 (comment)).

@edwardw
Copy link
Contributor Author

edwardw commented Mar 4, 2014

Ok, I removed the last commit about specialising hasher.

Regarding the slight performance issue, the vector element in question is a tuple (Ident, Name), 12 bytes in memory. The new vec_ng always doubles the vector size if running out of capacity, while vec only increases it modestly by 1. In the hygiene let smoke test, there is 5000 elements in the renaming list at the peak. So vec_ng has 8192 capacity or 8192 * 12 = 96k, vec on the other hand only uses roundup(5000 * 12) = 64k, it could be that vec is more CPU L1/L2 cache friendly in this case.

In any case, the performance difference is not that significant.

@huonw
Copy link
Member

huonw commented Mar 4, 2014

while vec only increases it modestly by 1

I don't believe that is true: .push calls .reserve_additional which calls .reserve which rounds up to the next power of two. i.e. it does double.

(Increasing the capacity by just 1 each time would cause quadratic runtime, due to having to realloc O(n) times.)

@edwardw
Copy link
Contributor Author

edwardw commented Mar 4, 2014

You are right, I should have been more thorough.

@edwardw
Copy link
Contributor Author

edwardw commented Mar 4, 2014

To be absolutely certain, I reverted Vec<T> to ~[T] on top the rebased commits and run smoke test again. The performance difference is neglectable; I must've seen some ghost numbers.

@edwardw
Copy link
Contributor Author

edwardw commented Mar 5, 2014

@jbclements comments?

@jbclements
Copy link
Contributor

Aw, geez. This whole thread is something I really have not had the time to keep up with. You finally got my attention, though, so I have at least one big question. Why do you need separate tables for lifetimes? Can you show me a concrete example where having a single table would cause a problem? I believe that the existing setup handles different kinds of bindings pretty transparently.

@edwardw
Copy link
Contributor Author

edwardw commented Mar 5, 2014

Here is the example from the original bug report:

fn main() {
    let mut foo = ~[];
    'foo: for i in [1, 2, 3].iter() {
        foo.push(i);
    }
}

One consequence of adding hygiene label support is that now label foo will shadow local var foo. So I come up with the idea of managing a separate table for lifetime since loop labels are lifetimes at token level. That way, local vars and loop labels with same name will not clash.

@jbclements
Copy link
Contributor

no... why would label foo shadow the let binding? One's a binding, one's a lifetime. The lifetime shouldn't end up in an environment during name resolution. I'm worried that something is seriously wrong here.

@edwardw
Copy link
Contributor Author

edwardw commented Mar 5, 2014

Loop labels are lifetime at token level, but they become ast::Ident at AST level. So the label renamings will show up in the pending renaming list and be picked up by any other Ident in the loop body.

@jbclements
Copy link
Contributor

@edwardw Grr... okay, gears grinding. You may be right. Lemme think about it more.

@jbclements
Copy link
Contributor

@edwardw Okay, added a lengthy-ish comment to issue #12512 . Let me know if it makes sense to you.

@huonw
Copy link
Member

huonw commented Mar 5, 2014

@edwardw If you felt like it, you could probably split out the general fixes commits of this and submit them as a separate PR, so that the perf improvements & FIXME-removals aren't blocked on the decision about #12512.

@edwardw
Copy link
Contributor Author

edwardw commented Mar 5, 2014

I split general improvements over the mtwt code into #12711.

Maintains two renaming tables, one for regular idents and one for
lifetime idents (a loop label is of lifetime kind) so that let bindings
and loop labels with same name won't clash.

Closes rust-lang#12512.
@alexcrichton
Copy link
Member

Closing due to inactivity. It sounds like the way to solve #12512 has a few options, and the solution may need to get settled on before making more progress.

It seems like a serious bug though, and I'd love to see it fixed!

bors added a commit to rust-lang-ci/rust that referenced this pull request Jul 25, 2022
flip1995 pushed a commit to flip1995/rust that referenced this pull request Apr 4, 2024
make sure checked type implements `Try` trait when linting [`question_mark`]

(indirectly) fixes: rust-lang#12412 and fixes: rust-lang#11983

---

changelog: make sure checked type implements `Try` trait when linting [`question_mark`]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Loop label hides variables with same name
5 participants