Jump to page: 1 2 3
Thread overview
New operators opStaticIndex and friends
May 14, 2019
Q. Schroll
May 14, 2019
H. S. Teoh
May 15, 2019
Q. Schroll
May 15, 2019
Exil
May 15, 2019
Q. Schroll
May 15, 2019
Simen Kjærås
May 15, 2019
Exil
May 16, 2019
Simen Kjærås
May 16, 2019
Exil
May 16, 2019
Simen Kjærås
May 16, 2019
Exil
May 17, 2019
Q. Schroll
May 17, 2019
Exil
May 18, 2019
Q. Schroll
May 15, 2019
Q. Schroll
May 14, 2019
angel
May 14, 2019
H. S. Teoh
May 14, 2019
Q. Schroll
May 14, 2019
Ahmet Sait
May 14, 2019
H. S. Teoh
May 14, 2019
Ahmet Sait
May 14, 2019
H. S. Teoh
May 14, 2019
Q. Schroll
May 15, 2019
Manu
May 14, 2019
I whish, D has these operators:

opStaticIndex
opStaticIndexAssign
opStaticIndexOpAssign
opStaticSlice

When used, they look the same way as opIndex and friends, but the stuff between [ and ] would be bound to template parameters (instead of runtime parameters) and be known at compile time. Basically, this allows to implement static indexing the same way it is possible for an AliasSeq.

Furthermore, it is possible to have static and dynamic indexing next to each other. This is supported by static-size arrays (e.g. int[3]) only: https://run.dlang.io/is/99g01e

Implementing e.g. tuple-like structures, it would be valuable to have both. Dynamic indexing would only be present (design by introspection) if the contents support it.

  struct S
  {
    enum int opStaticIndex(int index) = index + 1;
    int opIndex(int index) { return index + 1; }

    enum size_t[2] opStaticSlice(size_t i, size_t j) = [ i, j ];

    void opStaticIndexAssign(int index)(int value);
    void opStaticIndexAssign(int i, int j)(int value);
    void opStaticIndexAssign(size_t[2] slice)(int value);
    void opStaticIndexOpAssign(string op, int index)(int value);
  }

  S s;
  int i = s[0]; // rewrites to s.opStaticIndex!(0), as 0 can be used for the template parameter
  int j = s[i]; // rewrites to s.opIndex(i), as i cannot be read at compile-time

The "Static" variants would be preferred when determining which one to lower to.

s[0] = 1; // rewrites to s.opStaticIndexAssign!(0)(1)
s[0, 2] = 1; // rewrites to s.opStaticIndexAssign!(0, 2)(1)
s[0 .. 2] = 1; // rewrites to s.opStaticIndexAssign!(s.opStaticSlice!(0, 2))(1)
s[0] += 1; // rewrites to s.opStaticIndexOpAssign!("+", 0)(1)
s[0, 2] += 1; // rewrites to s.opStaticIndexOpAssign!("+", 0, 2)(1)

Currently, there is no way to implement custom compile-time indexing. Only using alias-this to an AliasSeq can do something, but is very limited. The alias-this is shadowed by any presence of opIndex.

An alternative would be a `static` or `enum` storage class for function runtime parameters that only bind to values known at compile-time.
These have been proposed years ago and would not only solve this but also format!"fmt" vs. format(fmt) and friends.

I have no knowledge about the DMD implementation, but I'd intuitively expect that new operator rewrites would be much less work to implement.

Do you want it? Do you see problems? Should I clarify something? Is it worth writing a DIP?
May 14, 2019
On Tue, May 14, 2019 at 03:49:51PM +0000, Q. Schroll via Digitalmars-d wrote:
> I whish, D has these operators:
> 
> opStaticIndex
> opStaticIndexAssign
> opStaticIndexOpAssign
> opStaticSlice
> 
> When used, they look the same way as opIndex and friends, but the stuff between [ and ] would be bound to template parameters (instead of runtime parameters) and be known at compile time. Basically, this allows to implement static indexing the same way it is possible for an AliasSeq.

+1.  I'd love to have this feature too.


T

-- 
Don't throw out the baby with the bathwater. Use your hands...
May 14, 2019
On Tuesday, 14 May 2019 at 15:49:51 UTC, Q. Schroll wrote:
> I whish, D has these operators:
>
> opStaticIndex
> opStaticIndexAssign
> opStaticIndexOpAssign
> opStaticSlice
>
> When used, they look the same way as opIndex and friends, but the stuff between [ and ] would be bound to template parameters (instead of runtime parameters) and be known at compile time. Basically, this allows to implement static indexing the same way it is possible for an AliasSeq.
>
> Furthermore, it is possible to have static and dynamic indexing next to each other. This is supported by static-size arrays (e.g. int[3]) only: https://run.dlang.io/is/99g01e
>
> Implementing e.g. tuple-like structures, it would be valuable to have both. Dynamic indexing would only be present (design by introspection) if the contents support it.
>
>   struct S
>   {
>     enum int opStaticIndex(int index) = index + 1;
>     int opIndex(int index) { return index + 1; }
>
>     enum size_t[2] opStaticSlice(size_t i, size_t j) = [ i, j ];
>
>     void opStaticIndexAssign(int index)(int value);
>     void opStaticIndexAssign(int i, int j)(int value);
>     void opStaticIndexAssign(size_t[2] slice)(int value);
>     void opStaticIndexOpAssign(string op, int index)(int value);
>   }
>
>   S s;
>   int i = s[0]; // rewrites to s.opStaticIndex!(0), as 0 can be used for the template parameter
>   int j = s[i]; // rewrites to s.opIndex(i), as i cannot be read at compile-time
>
> The "Static" variants would be preferred when determining which one to lower to.
>
> s[0] = 1; // rewrites to s.opStaticIndexAssign!(0)(1)
> s[0, 2] = 1; // rewrites to s.opStaticIndexAssign!(0, 2)(1)
> s[0 .. 2] = 1; // rewrites to s.opStaticIndexAssign!(s.opStaticSlice!(0, 2))(1)
> s[0] += 1; // rewrites to s.opStaticIndexOpAssign!("+", 0)(1)
> s[0, 2] += 1; // rewrites to s.opStaticIndexOpAssign!("+", 0, 2)(1)
>
> Currently, there is no way to implement custom compile-time indexing. Only using alias-this to an AliasSeq can do something, but is very limited. The alias-this is shadowed by any presence of opIndex.
>
> An alternative would be a `static` or `enum` storage class for function runtime parameters that only bind to values known at compile-time.
> These have been proposed years ago and would not only solve this but also format!"fmt" vs. format(fmt) and friends.
>
> I have no knowledge about the DMD implementation, but I'd intuitively expect that new operator rewrites would be much less work to implement.
>
> Do you want it? Do you see problems? Should I clarify something? Is it worth writing a DIP?


You say:
int i = s[0]; // rewrites to s.opStaticIndex!(0), as 0 can be
int j = s[i]; // rewrites to s.opIndex(i), as i cannot be read at compile-time

But actually, in this example, s[i] CAN be figured out at compile-time.
What is even more problematic is that the ability of the compiler to figure out the value of 'i' and s[i] at compile-time might depend on the optimization level, thus you cannot be sure whether s.opStaticIndex!(1) or s.opIndex(i) will be called.

IMHO, s[0] is already calculated in compile-time when appropriate optimization level is invoked ... at least it should've been.
May 14, 2019
On Tue, May 14, 2019 at 05:27:25PM +0000, angel via Digitalmars-d wrote: [...]
> You say:
> int i = s[0]; // rewrites to s.opStaticIndex!(0), as 0 can be
> int j = s[i]; // rewrites to s.opIndex(i), as i cannot be read at
> compile-time
> 
> But actually, in this example, s[i] CAN be figured out at compile-time.  What is even more problematic is that the ability of the compiler to figure out the value of 'i' and s[i] at compile-time might depend on the optimization level, thus you cannot be sure whether s.opStaticIndex!(1) or s.opIndex(i) will be called.
[...]

I think you're being misled by the ambiguous term "compile-time", which can refer to very different things. See:

	https://wiki.dlang.org/User:Quickfur/Compile-time_vs._compile-time

In D, indexing with a variable is not considered "compile-time" in the sense of AST manipulation (see above article for definition), although it *can* be evaluated in CTFE.  AIUI, opStaticIndex is intended to be used at the AST manipulation phase, since the usual "runtime" array indexing is already supported in CTFE.

This has nothing to do with optimization levels, which happens in codegen, long past any meaningful usage of compile-time computation as far as AST manipulation or CTFE is concerned.


T

-- 
Too many people have open minds but closed eyes.
May 14, 2019
On Tuesday, 14 May 2019 at 15:49:51 UTC, Q. Schroll wrote:
> I whish, D has these operators:
>
> opStaticIndex
> opStaticIndexAssign
> opStaticIndexOpAssign
> opStaticSlice

I think this is solving the wrong problem. If compiler provided means to check whether a parameter is known at compile time easily & consistently these would not be necessary at all. Of course, talk is cheap and actually implementing such new __traits is rather compilcated.
May 14, 2019
On Tuesday, 14 May 2019 at 17:27:25 UTC, angel wrote:
> You say:
> int i = s[0]; // rewrites to s.opStaticIndex!(0), as 0 can be
> int j = s[i]; // rewrites to s.opIndex(i), as i cannot be read at compile-time
>
> But actually, in this example, s[i] CAN be figured out at compile-time.

You get it wrong: The fact that `i` can be figured out by some clever compiler is irrelevant. You couldn't assign it to some static immutable or enum. You couldn't use it as a template parameter. This is not about clever optimizations, it is about wether the spec says, the value is present at compile time.

> What is even more problematic is that the ability of the compiler to figure out the value of 'i' and s[i] at compile-time might depend on the optimization level, thus you cannot be sure whether s.opStaticIndex!(1) or s.opIndex(i) will be called.
>
> IMHO, s[0] is already calculated in compile-time when appropriate optimization level is invoked ... at least it should've been.

If `seq` is a non-empty AliasSeq, you can use seq[0]. You cannot use seq[i] for `i` defined as above. The spec says that `i`'s value is determined at runtime, i.e. when the compiled program is executed. If the compiler's optimizer can figure out that it knows the value beforehand, it won't make the thing compile.

When talking about language specification, one rarely considers optimizations.
May 14, 2019
On Tue, May 14, 2019 at 06:13:18PM +0000, Ahmet Sait via Digitalmars-d wrote:
> On Tuesday, 14 May 2019 at 15:49:51 UTC, Q. Schroll wrote:
> > I whish, D has these operators:
> > 
> > opStaticIndex
> > opStaticIndexAssign
> > opStaticIndexOpAssign
> > opStaticSlice
> 
> I think this is solving the wrong problem. If compiler provided means to check whether a parameter is known at compile time easily & consistently these would not be necessary at all. Of course, talk is cheap and actually implementing such new __traits is rather compilcated.

Again, people are getting confused by the overloaded term "compile-time". People really need to read this:

	https://wiki.dlang.org/User:Quickfur/Compile-time_vs._compile-time

(Sorry for the shameless self-plug.)

opStaticIndex is necessary because it pertains to the *AST manipulation* phase of compilation, whereas opIndex pertains the executable phase (CTFE and/or runtime).  It's meaningless to "check whether a parameter is known at compile time" because you cannot go back to the AST manipulation stage after you have passed semantic analysis, which is when types are resolved and things like VRP are applied.


T

-- 
Beware of bugs in the above code; I have only proved it correct, not tried it. -- Donald Knuth
May 14, 2019
On Tuesday, 14 May 2019 at 18:13:18 UTC, Ahmet Sait wrote:
> On Tuesday, 14 May 2019 at 15:49:51 UTC, Q. Schroll wrote:
>> I whish, D has these operators:
>>
>> opStaticIndex
>> opStaticIndexAssign
>> opStaticIndexOpAssign
>> opStaticSlice
>
> I think this is solving the wrong problem.

Maybe. I'll try to clarify what problem is being solved.

Current state of D: Let seq be some AliasSeq. Indexing it looks like this:

    auto value = seq[i];

Here, the compiler must evaluate `i` at compile-time (cf. H. S. Teoh's link) to even get the type of value correctly.

Let arr be some slice (aka. "dynamic array"), e.g. a value of int[]. Indexing it looks like this:

    auto value = arr[i];

Wow, they look identically, but mean completely different things! What an incident.

You can make the latter work in your custom type (notably random-access ranges) by providing opIndex and companions.

However, you cannot provide the former functionality for your custom type. The suggested operators would solve that without interrupting the rest of the language.

> If compiler provided means to check whether a parameter is known at compile time easily & consistently these would not be necessary at all

What kind of parameter? A template parameter is known at compile-time by definition and a function (run-time) parameter by definition is not. The new operators won't change that.

> Of course, talk is cheap and actually implementing such new __traits is rather compilcated.

This is no kind of __traits. I don't think the problem could be solved nicely with such __traits if it existed.

The alternative were a parameter *storage class*:

    T opIndex(static size_t i); // alternative to opStaticIndex
    T opIndex(       size_t i);

An illustrative example:

    void format(static string fmt, ...); //1
    void format(       string fmt, ...); //2

You'd have both overloads. //1 is viable, when the first parameter can be evaluated at compile-time. It could check at compile-time if the arguments' types match the format. //2 is always viable, but //1 is considered a better match for a compile-time format string. The `static` storage class would make the parameter semantically equivalent to a template parameter inside the `format` function. Outside, it would be called using the same syntax, i.e.

    string s1 = format("The %s-th component", i); // calls //1
    string s2 = format(readln(), i); // calls //2

You basically could overload on the compile-time availableness of parameters. There is no __traits to be involved. Even without compiler knowledge, I strongly believe that this is more complicated, apart from being a major change of the language.
May 14, 2019
On Tuesday, 14 May 2019 at 18:35:51 UTC, H. S. Teoh wrote:
> On Tue, May 14, 2019 at 06:13:18PM +0000, Ahmet Sait via Digitalmars-d wrote:
>> On Tuesday, 14 May 2019 at 15:49:51 UTC, Q. Schroll wrote:
>> > I whish, D has these operators:
>> > 
>> > opStaticIndex
>> > opStaticIndexAssign
>> > opStaticIndexOpAssign
>> > opStaticSlice
>> 
>> I think this is solving the wrong problem. If compiler provided means to check whether a parameter is known at compile time easily & consistently these would not be necessary at all. Of course, talk is cheap and actually implementing such new __traits is rather compilcated.
>
> Again, people are getting confused by the overloaded term "compile-time". People really need to read this:
>
> 	https://wiki.dlang.org/User:Quickfur/Compile-time_vs._compile-time
>
> (Sorry for the shameless self-plug.)
>
> opStaticIndex is necessary because it pertains to the *AST manipulation* phase of compilation, whereas opIndex pertains the executable phase (CTFE and/or runtime).  It's meaningless to "check whether a parameter is known at compile time" because you cannot go back to the AST manipulation stage after you have passed semantic analysis, which is when types are resolved and things like VRP are applied.
>
>
> T

I've read it before, and read it again now just to be sure. Obviously such functions (the ones that check whether a variable is known at CT) need to be templates and get instantiated on every call to actually work.

Something close to this:

auto opIndex()(size_t i)
{
    enum types = [ "double", "long", "int" ];
    static if (__traits(isKnownAtCT, i))
    {
        static if (i < types.length)
            return mixin("cast(" ~ types[i] ~ ")i");
        else
            return "Here be dragons";
    }
}
May 14, 2019
On Tue, May 14, 2019 at 08:07:03PM +0000, Ahmet Sait via Digitalmars-d wrote: [...]
> I've read it before, and read it again now just to be sure. Obviously such functions (the ones that check whether a variable is known at CT) need to be templates and get instantiated on every call to actually work.

Templates are not instantiated once per call.  They are only instantiated once per unique combination of argument types.


> Something close to this:
> 
> auto opIndex()(size_t i)

This doesn't work, as the compiler sees `i` as a runtime parameter. When compiling this code, the compiler doesn't know (nor care) where the argument might have come from. It can't, because this function could potentially be called from an external module separately compiled, or even bound only at runtime via a dynamic library load.

Also, since the template parameter list is empty, this template will only be instantiated once throughout the entire program. So the static if below won't work as you want it to.


> {
>     enum types = [ "double", "long", "int" ];
>     static if (__traits(isKnownAtCT, i))
>     {
>         static if (i < types.length)
>             return mixin("cast(" ~ types[i] ~ ")i");
>         else
>             return "Here be dragons";
>     }
> }

Something closer to what you're looking for might be this:

	auto opIndex(alias i)() if (is(typeof(i) == size_t) {
		...
	}

This will get instantiated once per call, but will require !() syntax rather than the usual, so the way the [] operator is overloaded will have to be extended.  Also, `i` in this scope will refer to the actual variable in the caller's scope, so if the function body modifies it you could get very strange results.

And I"m not 100% sure this will work for all possible argument types; the alias parameter may not accept arbitrary expressions due to various limitations, so you might end up with the strange situation that opIndex!0 works but opIndex!(1-1) doesn't.


T

-- 
English is useful because it is a mess. Since English is a mess, it maps well onto the problem space, which is also a mess, which we call reality. Similarly, Perl was designed to be a mess, though in the nicest of all possible ways. -- Larry Wall
« First   ‹ Prev
1 2 3