Proposal for design of 'scope' (Was: Re: Opportunities for D)

via Digitalmars-d digitalmars-d at puremagic.com
Sat Jul 12 11:01:36 PDT 2014


On Friday, 11 July 2014 at 21:04:05 UTC, H. S. Teoh via 
Digitalmars-d wrote:
> On Thu, Jul 10, 2014 at 08:10:36PM +0000, via Digitalmars-d 
> wrote:
> Hmm. Seems that you're addressing a somewhat wider scope than 
> what I had
> in mind. I was thinking mainly of 'scope' as "does not escape 
> the body
> of this block", but you're talking about a more general case of 
> being
> able to specify explicit lifetimes.
>

Indeed, but it includes what you're suggesting. For most use 
cases, just `scope` without an explicit lifetime annotation is 
fully sufficient.

> [...]
>> A problem that has been discussed in a few places is safely 
>> returning
>> a slice or a reference to an input parameter. This can be 
>> solved
>> nicely:
>> 
>>     scope!haystack(string) findSubstring(
>>         scope string haystack,
>>         scope string needle
>>     );
>> 
>> Inside `findSubstring`, the compiler can make sure that no 
>> references
>> to `haystack` or `needle` can be escape (an unqualified 
>> `scope` can be
>> used here, no need to specify an "owner"), but it will allow 
>> returning
>> a slice from it, because the signature says: "The return value 
>> will
>> not live longer than the parameter `haystack`."
>
> This does seem to be quite a compelling argument for explicit 
> scopes. It
> does make it more complex to implement, though.
>
>
> [...]
>> An interesting application is the old `byLine` problem, where 
>> the
>> function keeps an internal buffer which is reused for every 
>> line that
>> is read, but a slice into it is returned. When a user naively 
>> stores
>> these slices in an array, she will find that all of them have 
>> the same
>> content, because they point to the same buffer. See how this is
>> avoided with `scope!(const ...)`:
>
> This seems to be something else now. I'll have to think about 
> this a bit
> more, but my preliminary thought is that this adds yet another 
> level of
> complexity to 'scope', which is not necessarily a bad thing, 
> but we
> might want to start out with something simpler first.

It's definitely an extension and not as urgently necessary, 
although it fits well into the general topic of borrowing: 
`scope` by itself provides mutable borrowing, but `scope!(const 
...)` provides const borrowing, in the sense that another object 
temporarily takes ownership of the value, so that the original 
owner can only read the object until it is "returned" by the 
borrowed value going out of scope. I mentioned it here because it 
seemed to be an easy extension that could solve an interesting 
long-standing problem for which we only have workarounds today 
(`byLineCopy` IIRC).

And I have to add that it's not completely thought out yet. For 
example, might it make sense to have `scope!(immutable ...)`, 
`scope!(shared ...)`, and if yes, what would they mean...

>
>
> [...]
>> An open question is whether there needs to be an explicit 
>> designation
>> of GC'd values (for example by `scope!static` or `scope!GC`), 
>> to say
>> that a given values lives as long as it's needed (or 
>> "forever").
>
> Shouldn't unqualified values already serve this purpose?
>
>

Likely yes. It might however be useful to contemplate, especially 
with regards to allocators.

> [...]
>> Now, for the problems:
>> 
>> Obviously, there is quite a bit of complexity involved. I can 
>> imagine
>> that inferring the scope for templates (which is essential, 
>> just as
>> for const and the other type modifiers) can be complicated.
>
> I'm thinking of aiming for a design where the compiler can 
> infer all
> lifetimes automatically, and the user doesn't have to. I'm not 
> sure if
> this is possible, but based on what Walter said, it would be 
> best if we
> infer as much as possible, since users are lazy and are 
> unlikely to be
> thrilled at the idea of having to write additional annotations 
> on their
> types.

I agree. It's already getting ugly with `const pure nothrow @safe 
@nogc`, adding another annotation should not be done 
lightheartedly. However, if the compiler could infer all the 
lifetimes (which I'm quite sure isn't possible, see the 
haystack-needle example), I don't see why we'd need `scope` at 
all. It would at most be a way not to break backward 
compatibility, but that would be another case where you could say 
that D has it backwards, like un- at safe by default...

>
> My original proposal was aimed at this, that's why I didn't put 
> in
> explicit lifetimes. I was hoping to find a way to define things 
> such
> that the lifetime is unambiguous from the context in which 
> 'scope' is
> used, so that users don't ever have to write anything more than 
> that.
> This also makes the compiler's life easier, since we don't have 
> to keep
> track of who owns what, and can just compute the lifetime from 
> the
> surrounding context. This may require sacrificing some 
> precision in
> lifetimes, but if it helps simplify things while still giving 
> adequate
> functionality, I think it's a good compromise.

I agree it looks a bit intimidating at first glance, but as far 
as I can tell it should be relatively straightforward to 
implement. I'll explain how I think it could be done:

The obvious things: The parser needs to recognize the new syntax, 
and scope needs to be turned into a type modifier and stored in 
the internal data structures accordingly.

It is then possible to define a hierarchy of lifetimes. At the 
top are global and static variables and the GC heap 
(`scope!static` or just unannotated), then the come function 
parameters, then local variables in function bodies, and finally 
local variables in lower scopes like `if` blocks. This is purely 
based on lexical scope and order of declaration (local variables 
are destroyed in inverse order of construction, for example); it 
can be derived from the AST. Furthermore, it is a strict 
hierarchy; lifetimes higher in the hierarchy are strict super 
sets of lower lifetimes.

A variables effective lifetime is then its place in this 
hierarchy, or the lifetime of its owner if one is specified.

Once that's done, the semantic phase needs to be extended to 
check for scope correctness. This seems complicated, but actually 
needs to touch only a few places. Any time a scope value is 
copied, by assignment, returning from a function, passing to a 
function, throwing, and what else I may have missed, the compiler 
needs to check that the destination's effective lifetime is not 
wider than that of the source.

For function calls, an additional step is necessary, but it isn't 
really complicated either. Let's take `findSubstring` as an 
example:

     scope!haystack(string) findSubstring(
         scope string haystack,
         scope string needle
     );

     void foo() {
         string[$] h = "Hello, world!";
         auto found = findSubstring(h, ", ");
         // `typeof(found)` is now `scope!h`
     }

As owners in function signatures may refer to other parameters 
(or `this`), the compiler needs to match up these parameters with 
what is passed in, and substitute them accordingly for type 
deduction (only for `auto` return values).

And that's it, AFAICS. Notice that none of this requires flow 
control analysis or inter-procedural things, it can all be 
decided locally at the place of assignment/calling/etc.

>
>
> [...]
>> I also have a few ideas about owned types and move semantics, 
>> but this
>> is mostly independent from borrowing (although, of course, it
>> integrates nicely with it). So, that's it, for now. Sorry for 
>> the long
>> text. Thoughts?
>
> It seems that you're the full borrowed reference/pointer 
> problem, which
> is something necessary. But I was thinking more in terms of the 
> baseline
> functionality -- what is the simplest design for 'scope' that 
> still
> gives useful semantics that covers most of the cases? I know 
> there are
> some tricky corner cases, but I'm wondering if we can somehow 
> find an
> easy solution for the easy parts (presumably the more common 
> parts),
> while still allowing for a way to deal with the hard parts.
>
> At least for now, I'm thinking in the direction of finding 
> something
> with simple semantics that, at the same time, produces complex
> (interesting) effects when composed, that we can use to solve 
> the
> borrowed pointer problem.

I already wrote this in a reply to Walter. I believe in some 
cases we can allow automatic borrowing without any annotation at 
all, not even bare `scope`. The most obvious examples are pure 
functions with signatures that guarantee that nothing can be 
escaped from them:

     void foo(int[] p) pure;    // obvious, function has no 
opportunity
                                // to keep a reference to `p`
     int bar(int[] p) pure;     // returns an `int` but that's a 
value
                                // type, and that's ok
     int[] baz(const(int)[] p) pure;
                                // the return type is not `const` 
and thus
                                // cannot come from `p`

Maybe there are some cases with non-pure functions, too. But on 
the other hand, I also think that in the end we won't get around 
introducing explicit annotations, because the above rules can 
never cover enough cases to disregard the remaining ones.

Anyway, I don't believe that explicit annotations will be needed 
often enough to turn the users away. It will be mostly library 
writers who have to use them, and Phobos can set a good example 
there and work out a good style, just as it has done for other 
matters.

It also helps to take a glance at Rust's standard library, to see 
how frequent or infrequent lifetime annotations will be. They 
keep popping up here and there, but they are not littered all 
over the source code. They're frequent enough to confirm my 
suspicion that they cannot be disregarded, but they're also 
infrequent enough not to be an annoyance. (I only looked at a few 
modules, though.)


More information about the Digitalmars-d mailing list