or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
2024-02-29
Equivalence of match and block with let
Mara: With the newly proposed rules, would the following be equivalent?
and
Niko: yes.
Mara: Let's put that clearly in the RFC. Important equivalence.
Xiang: to extend it with
let else
there is equivalence betweenand
… but wait it is intricate…
Niko: consider this example…
Niko: equivalence is
Mara: it's let, not super let:
Niko:
Mara: This errors today:
Conclusion:
Mara's proposal is:
[&long(), short().as_ref()]
)timeline and questions:
Mara: Without changing match/if-lets, the only edition change is tail expressions. That's a tiny edition change.
Niko: yes but it can cause significant user confusion in my experience.
Niko: I think it still makes sense together in one RFC.
Mara: super let is simpler to explain when we skip the new match rules, since it only has an effect in a
let
statement.Mara: If we separate https://github.com/rust-lang/rust/pull/121346 out of the RFC, then the RFC is only about redefining what "long" and "short" are, not changing how to decide between "long" and "short".
Splitting off a small part
Mara: https://github.com/rust-lang/rust/pull/121346
Niko: doesn't that change beahviour of existing code?
Mara: Don't think so.
Xiang: it only accepts new code
Niko to review.
2024-01-25
Lifetimes / scopes
let x = foo.as_ref() // foo is freed "immediately"
Rules and
let { .. }
on Zulip)let x = &temp();
and{ .. match &temp() {}; .. /*here*/ }
Use cases we are trying to solve
Proposed rules
ResultScope(E)
andIntermediateScope(E)
to indicate the result scope of some expressionE
super {E}
expression,E
is evaluated into a temporary with "result scope" lifetimeLet x = Expr
– result scope is the enclosing blockTC: The pin example, for our reference:
extend a = asdf in asdfasdfasdfs(&a)
{ super let a = asdf; asdfasdfasdfs(&a) }
Formalized version (from below)
Definitions
foo(bar, baz)
.if
,if let
,while
andmatch
. These produce results that will be examined to determine which "arms" to execute next. In the case ofmatch
andif let
, parts of the scrutinee will always be stored into values that persist into those arms, but parts of the scrutinee may not escape into the overall result of the match. In the case of if/while, the result of the scrutinee is a boolean, so that consideration does not apply.StructName { f1: $e1, f2: $e2 }
or($e1, $e2)
are result-producing&foo()
) is a result-producing expressionVariantName(x)
andTupleStruct(x)
are treated like any other function call (i.e., intermediate expressions), but they are in fact result-producing. We propose to change them in Rust 2024 to be result-producing, i.e.,StructName { 0: 22 }
andStructName(22)
behave the same. There is one potential downside, though, it may mean that changing from tuple struct to afn
with same name is a breaking change (is it already?).$value
(one that does not name a place in memory) in a location where a reference is required:&$value
/&mut $value
–$value.m()
– whenm
is auto-refResultScope(E)
andIntermediateScope(E)
to indicate the result scope of some expressionE
E
, and recursing into one of its direct subexpressionsS
…S
is categorized as an intermediate subexpression:ResultScope(S) = IntermediateScope(E)
– because the value might not be referenced in the resultIntermediateScope(S) = IntermediateScope(E)
S
is categorized as a scrutinee subexpression:ResultScope(S) = IntermediateScope(E)
– because the value does escape into the bindings for arms, but might not escape into the result of those armsIntermediateScope(S) = S
– unlike other subexpressions which use the same intermediate scope asS
, we prefer to drop temporaries in scrutinees before scrutinizing the result or entering the arms (e.g.,match &lock().unwrap() {...}
will drop the lock before matching). This is because of our first tenet: matches and if lets, in particular, often have arms that do a lot of things. Having shorter scopes here is important for reliability and avoids a common source of bugs in Rust today, hence we override our general preference for larger scopes.S
is categorized as a result-producing subexpression:ResultScope(S) = ResultScope(E)
– the result ofS
is being stored in the same placeIntermediateScope(S) = E if E is a block, else IntermediateScope(E)
let $pat = $expr
:$pat
are given the scope of the enclosing block$expr
is evaluated with its:let
statementsuper let $pat = $expr
$pat
are given the result scope of the enclosing block$expr
is evaluated with its:super let
statement&temp()
ortemp().m()
)…temp()
appears in a result-producing location, it is given the result scopeUse cases we are trying to solve
2024-01-18
Feedback on
super let
ideaMost of the actionable feedback is roughly either:
For 1, we need to consider other words.
super
is the only existing keyword that makes sense, but we could add a new keywordFor 2, it would look like this:
For 3, it would looks like this:
Mara's opinion: continue with
super { }
blocks, but perhaps with a different keyword.(Either in stead of or in addition to
super let
statements.)Niko: Agree, with just
super {}
notsuper let
, since it's niche.RFC and next steps
Need to hurry to make it into the 2024 edition. Next steps:
RFC Draft: https://hackmd.io/wU_CYnUeT7G7hYazna6vDQ
Motivation section not complete/clear yet. Maybe take some stuff from https://blog.m-ou.se/super-let/
Adapt RFC for super blocks instead of super let.
How does super expression work?
super{expr} == expr
produces the same value but has different temporary rules&super{expr}
lives longermatch super{expr} { ref x => .. } // same thing
super { 1 } + 2
pointless, lint against thissuper { Some(22) }.as_ref() == Some(&super{22}) // (more or less equivalent)
Status of the PR
Hasn't been much review, contains implementation for
super let
PR: https://github.com/rust-lang/rust/pull/119043
NEXT STEPS
super { }
expressions – Xiang2023-11-30
short-circuiting
Mara: Shouldn't that be equivalent to this?
Mara: i think super let in init1 and init2 should work (extend to entire body of if), and that super let in test1 and test2 are meaningless (compiler should suggest removing
super
). To be consistent with a regularif test1() && test2()
.Different than:
Here, test1() and test2() can have a super let that extends to the entire block?
Can't invoke TLE here actually. Because
true
as a pattern has no materialised variable binding. :(Alternative to super let:
&'a expr
How about
can that be written as
&'super
? Don't think so?2023-11-16
Xiang made progress on RFC
Refactor will be next
https://github.com/rust-lang/rust/pull/111725
notes
if
andmatch
scrutinees, such that givenif { super let x = ... }
, thex
is dropped before enteringif
body (that is, thesuper
is meaningless here)match
andif let
expressions, usingsuper let
is weirdmatch &foo() { ... }
– this should workmatch { super let f = foo(); &f } { }
– therefore this should workmatch
es andif let
s depend on today's behaviour.short-circuit operators
proposal was to change this to…
means that:
if e1 && e2 {
is now equivalent to the MIR forif e1 { if e2 {
, which changes how borrowck understands that code, seetests/ui/rfc-2497-if-let-chains/chains-without-let.rs
.could have some special rules around short-circuit operators…
a && b
is kind of likeif a { b } else { false }
so…you could justify it this waya || b
is kind of likeif a { true } else { b }
so…you could justify it this wayif mutex1.lock().condition() && mutex2.lock().condition() { .. }
-> fine and good to unlock asap.
if cell.write().update_for_a() || cell.write().update_for_b() { ... }
if foo.lock().acquire() && .... /* expects lock to be held */ {
if a != null && a.foo() {
clippy can today already suggest merging two
if
s into a singleif &&
. would be nice if those were exactly identical.issue: https://github.com/rust-lang/rust/issues/111583, https://github.com/rust-lang/rust/pull/111752
observations: the lhs/rhs
&&
and||
Conclusion: operands of short-circuit operators should be considered "scrutinee expressions", just like an
if
condition.Why?
if a && b {
andif a { if b }
, which is intuitive but also useful for if-let chains.if a { if b {
intoif a && b {
, for example, but that can be wrong in subtle ways todayif
but innermost shortcircuit operator – but not super-more, there's already a list of things that treat their subexpr as scrutinee and this feels like it belongs in that listedition migration
cases to be careful of…
match
orif let
/while let
scrutineewhile
orfor
&&
,||
)examples
match
short-circuit
random mara question
if _ && _ { .. }
in 2024 behaves the same asif _ { if _ { .. } }
let a = _ && _;
, also drops temporaries early?let a = if _ { _ } else { false }
random niko question
To what extent are these equivalent…
…and in particular with respect to super lock…
super
is meaningless in theif
, would result in a warning.crater run?
We need to check how often a
match
orif let
needs the current temporary lifetime rules.If there are many, we might need quite an advanced migration lint that doesn't generate ugly code.
tail expressions in blocks
Some() vs Some {}
if we only look at syntax, we can't distinguish between
Some()
andf()
.we can fix this though. treat variants differently than functions.
niko: we could allow functions to opt-in to this behaviour.
super let
lintcan we make suggestions to add
super
and removesuper
happen reliably in all cases?example:
other direction:
when would this trigger?
if
or other cases where it is does not change the scope at all2023-11-02
… expands to
lowers in hir to:
does it work for more complicated ones too?
print!("{0} {0:?}", a)
? that expands to amatch
.False alarm because it was reported by a buggy implementation
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →2023-10-19
"Value-expression" rules that Xiang does not like:
temporary lifetime extension:
Rust 2021 behavior:
StructLit { y: 0 }
above gets extended&<expr>
– the subexpression<expr>
is result-producingNevermind it's fine. :)
Draft RFC
Experimental description of the Oct 5 rules…
AST looks roughly like this:
for a given expression operator
Op
we categorize its subexpressions asand define
categories(Op) = Category*
, one for each expression. i.e.:categories(&) = [ResultProducing]
.categories(+) = [Intermediate, Intermediate]
.categories(Call(...)) = [Intermediate...]
categories(StructName(fieldname...)) = [ResultProducing...]
categories(Match(Pat...)) = [Scrutinee, ResultProducing...]
, i.e., the scrutinee is, well, a scrutinee, but each of the arm expressions are result-producing.For a given expression, we can reduce a set
result_producing(E)
of expressions that includes E and all of its (transitive) result-producing subexpressions. Soresult_producing(&Foo { x: bar(22) })
would include&Foo { .. }
,Foo { x: bar(22) }
, andbar(22)
. It does not includebar
or22
, because all subexpressions of call nodes are intermediate.Some expressions introducer temporaries. Those expressions are always part of some statement or the tail expression of a block. To determine the lifetime of temporaries:
Expr
–let pattern = E_init
–result_producing(E_init)
is end of enclosing blocksuper let pattern = Expr
result_producing(E_init)
are dropped in result-scope of blockE
:E_t
inresult_producing(E)
is stored into a temporary, that temporary lifetime is the result scope of the block.2023-10-05 rules
Trying to write out the rules below.
Tenets
We believe that…
let
that makes the lifetime clear. If you give a longer scope than the programmer needs, they may get deadlocks or other surprising runtime behavior. We prefer the former.E
, it should always be possible to write a block{ ... }
that behaves the same. For this to be true, we need to introduce some way within a block to create values that have the same scope as temporaries would inE
. This design achieves this. Specifically,{ super let x = temp(); &x }
and&temp()
are always equivalent.let x = &temp()
is clearly being stored intox
For example, we don't muck about with fine-grained scopes within individual statements for the most part$expr
inlet $pat = $expr
, but those rules don't depend on fine-grained details of the pattern$pat
(so i.e., if any part of a value escapes, we consider that equivalent to all parts escaping; but we do look to see if the value may sometimes never escape).Definitions
foo(bar, baz)
.if
,if let
,while
andmatch
. These produce results that will be examined to determine which "arms" to execute next. In the case ofmatch
andif let
, parts of the scrutinee will always be stored into values that persist into those arms, but parts of the scrutinee may not escape into the overall result of the match. In the case of if/while, the result of the scrutinee is a boolean, so that consideration does not apply.StructName { f1: $e1, f2: $e2 }
or($e1, $e2)
are result-producing&foo()
) is a result-producing expressionVariantName(x)
andTupleStruct(x)
are treated like any other function call (i.e., intermediate expressions), but they are in fact result-producing. We propose to change them in Rust 2024 to be result-producing, i.e.,StructName { 0: 22 }
andStructName(22)
behave the same. There is one potential downside, though, it may mean that changing from tuple struct to afn
with same name is a breaking change (is it already?).$value
(one that does not name a place in memory) in a location where a reference is required:&$value
/&mut $value
–$value.m()
– whenm
is auto-refResultScope(E)
andIntermediateScope(E)
to indicate the result scope of some expressionE
E
, and recursing into one of its direct subexpressionsS
…S
is categorized as an intermediate subexpression:ResultScope(S) = IntermediateScope(E)
– because the value might not be referenced in the resultIntermediateScope(S) = IntermediateScope(E)
S
is categorized as a scrutinee subexpression:ResultScope(S) = IntermediateScope(E)
– because the value does escape into the bindings for arms, but might not escape into the result of those armsIntermediateScope(S) = S
– unlike other subexpressions which use the same intermediate scope asS
, we prefer to drop temporaries in scrutinees before scrutinizing the result or entering the arms (e.g.,match &lock().unwrap() {...}
will drop the lock before matching). This is because of our first tenet: matches and if lets, in particular, often have arms that do a lot of things. Having shorter scopes here is important for reliability and avoids a common source of bugs in Rust today, hence we override our general preference for larger scopes.S
is categorized as a result-producing subexpression:ResultScope(S) = ResultScope(E)
– the result ofS
is being stored in the same placeIntermediateScope(S) = E if E is a block, else IntermediateScope(E)
let $pat = $expr
:$pat
are given the scope of the enclosing block$expr
is evaluated with its:let
statementsuper let $pat = $expr
$pat
are given the result scope of the enclosing block$expr
is evaluated with its:super let
statement&temp()
ortemp().m()
)…temp()
appears in a result-producing location, it is given the result scopeExamples
Escape-from-if
if
is equal to the enclosing block'b
'b
a
is a super let, so it is given the result scope of enclosing block, and hence has the scope'b
&temp()
Match-drops-lock-before-arm
Today the lock is held until end of statement enclosing the match, but we don't want that
Playground
Here, the result-scope of the main block is
'f
, the scope of the fn'm
is result-producing, so its result-scope is'f
; intermediate scope isb
's
has result scope of'b
and intermediate scope of's
something()
call has intermediate subexpressionx.lock().unwrap()
, so its result/intermediate scope is's
lock()
is therefore dropped at's
This differs from the behavior today, where the intermediate scope for match scrutinee is equal to the intermediate scope of the match.
Match-with-super-let
Today the lock is held until end of statement enclosing the match, but we don't want that
Playground
'f
, the scope of the fn'm
is result-producing, so its result-scope is'f
; intermediate scope isb
's
has result scope of'b
and intermediate scope of's
l
is assigned to the result scope ('b
), and hence will be dropped on exit from the blockmatch &temp()
today.Tail-expr-drops-early
Today this gives an error, but we don't want one anymore:
Playground
'f
, the scope of the fn'e1
is result-producing, so its result-scope is'f
; intermediate scope isb
*
expression's contents are intermediate so their result-scope is'b
x
(following the LIFO rule)2023-10-05
super let
is proposal to explicitly declare the extended temporary lifetimesIntuition:
Mara:
Niko: If possible, these should be the same:
One tricky case:
Mara: How about: ?
Conclusion: temporary lifetime extension should also work through if-else expressions.
Mara: So, this should compile:
surprising error today
Playground
In Rust 2021, it is equivalent to this today
and hence you get an error, but in Rust 2024 is equivalent to
automatic edition upgrade possible by using
super let
for temporariesInvariants we want
{super let x = temp(); &x}
and&temp()
are equivalentgiven
{&temp()}
,temp()
is in a temporary that lives long enough to be storedif condition { &temp() } else { &temp() }
is the samematch lock().foo { }
– for sure should droplock()
before entering armsmatch &lock().foo { ... }
?let x = ...
x
lives until end of block...
is the enclosing blocksuper let x = ...
x
lives until end of result scope of the enclosing block...
is the result scope of the enclosing blocktail expression of a block:
block for an if/else:
given a
&temp()
, store it in the result scopeWalking through the rules:
Niko's version:
Mara: How about this?
How long should
super let
live in match scrutinee?*
some examples around match:
This does not compile
Only thing that changes behavior:
2023-07-06
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →how Niko expected it to work (but it doesn't quite?):
but:
S_parent
receives lifetime extension?e.g.
The scope
's
is the said S_parent scope. It does not receive lifetime extensions. Nonethless, I believe it makes sense torules for match:
let x = match foo().as_ref() { x => x };
gives an errormatch ref_cell.write().bar() { }
drops refcell after callingbar()
match &id(22) { ... }
is an errormatch { super let guard = 22; &guard }
is also an errormatch ref_cell.write().bar() { }
unchangedmatch &id(22) { ... }
the temp forid(22)
is freed at end of innermost enclosing statementlet x = match &id(22) { v => v };
, because match has extended temporary lifetime, so does theid(22)
temporarymatch { super let guard = 22; &guard }
behaves the samematch ref_cell.write().bar() { }
unchangedmatch &id(22) { ... }
the temp forid(22)
is freed at end of innermost enclosing statementbut let x = match &id(22) { v => v };
is an errormatch { super let guard = 22; &guard }
behaves the sameyou can replace …
match <expr> { }
…with
this is kind of like the behavior we have today but different. almost the behavior we have today except that
f
may live longer in a case likelet x = { super let f = <expr>; match f { ... } }
, as it would be dropped at end of block, butmatch <expr> { ... }
would be dropped at end of statement ?Q: What is the behavior of
super let x = &foo()
again?Answer: currently, if they are ordinary temporaries, dropped at the end of the initializer; otherwise, if extended, they live same as the super let bindings.
Resolution: this behavior is implemented in the super-let draft today (2023-07-19)
2023-06-08
consideration, how should the
assert_eq
macro worktoday:
want to make sure a use like this
Today:
let $pat = $expr
$pat
are dropped at the end of the block$pat
is dropped$pat
is assignedsuper let $pat = $expr
$pat
are dropped at the end of the "result scope" of the surrounding block$pat
is dropped$pat
is assignedWe call the "temporary scope" of an expression to be when it would be dropped if it were made into a temporary.
For a given let initializer:
For a given super let initializer:
Intuition:
2023-05-18
Two parts
let super
Question marks:
let
or the bindingDefinition
The temporary scope (need better name) of an expression is
Defined as (in Rust 2024)…
super let x
) the scope of the letExpressions are extended by a let L if…
super let
at function scope is an errorRule of thumb
Role of thumb is that these are equivalent:
&foo()
{super let x = foo(); &x}
&<expr>
{super let x = &<expr>; x}
Tenets
&<expr>
and{super let x = &<expr>; x}
are equivalent&<rvalue>
and{super let x = <rvalue>; &x}
are equivalent, as a corrolaryDrop
runs later than you expectLitmus tests
format-args macro expansion
match discriminant
Question
Before
use cases
The
format_args
macroThe
pin
macroOr: track unsafeness by the span?
lifetime in match too long
lifetime must be extended
Rustc
macro_rules! assert_eq
move closure capture
https://marabos.nl/atomics/atomics.html#example-progress-reporting-from-multiple-threads
two cases
&dyn if pattern?
what people try first
what you can do but people never think of it until they are told
^ This trick is something many people don't realize is possible.
^ Nice for diagnostics. just "add super".
RFC 66
surprising error today
In Rust 2021, it is equivalent to this today
and hence you get an error, but in Rust 2024 is equivalent to
automatic edition upgrade possible by using
super let
for temporariesif let with lock
(https://marabos.nl/atomics/basics.html#lifetime-of-mutexguard)
This example should still compile, but now drop the lock right away. (Since pop doesn't return something that borrows the vec, it still compiles. If it was .first(), it'd start to give an error, unlike today.)
Proposal of sorts
if
){ expr }
drops temporaries once block exitssuper let
to introduce a value that lives until block is consumedWhat about…
what people "intuitively expect"?
let _ =
is same aslet i =
_
on its own is not a "pattern" but an anonymous identifierlet $ident = ..
, notlet $pattern =
, because that's how it gets taught.bar()
immediatelylet $pattern
(which seems a separate case fromlet $ident
)Mutex<()>
https://marabos.nl/atomics/basics.html#mutexes-in-other-languages
Maybe use
Mutex<Zst>
instead, put the relevant operations on the Zst.Mutex::unlock
(don't like it, but some people talked about this)
z