Thread overview
Contract error string generation is not implicitly @nogc
Sep 04, 2022
Ali Çehreli
Sep 04, 2022
IGotD-
Sep 04, 2022
Paul Backus
Sep 04, 2022
IGotD-
Sep 05, 2022
tsbockman
Sep 05, 2022
Ali Çehreli
Sep 12, 2022
Ali Çehreli
September 04, 2022
void foo(string s) @safe pure nothrow @nogc
in (s != "hello", "Invalid " ~ s) {
}

void main() {}

Error: cannot use operator `~` in `@nogc` function `deneme.foo`

But that operator is used only when we are about to throw an Error, which is supposed to imply we shouldn't continue with the program anyway. So, just one little GC allocation wouldn't hurt, right? :)

Is there any downside to allowing GC allocation in that case?

Otherwise, we have to throw away either @nogc annotation or the functionality of providing useful error messages.

Ali
September 04, 2022
On Sunday, 4 September 2022 at 14:06:44 UTC, Ali Çehreli wrote:
> void foo(string s) @safe pure nothrow @nogc
> in (s != "hello", "Invalid " ~ s) {
> }
>
> void main() {}
>
> Error: cannot use operator `~` in `@nogc` function `deneme.foo`
>
> But that operator is used only when we are about to throw an Error, which is supposed to imply we shouldn't continue with the program anyway. So, just one little GC allocation wouldn't hurt, right? :)
>

...but what if the GC is totally disabled or not even initialized? This might be the environment you'd want to use @nogc.
September 04, 2022
On Sunday, 4 September 2022 at 14:30:51 UTC, IGotD- wrote:
> On Sunday, 4 September 2022 at 14:06:44 UTC, Ali Çehreli wrote:
>> void foo(string s) @safe pure nothrow @nogc
>> in (s != "hello", "Invalid " ~ s) {
>> }
>>
>> void main() {}
>>
>> Error: cannot use operator `~` in `@nogc` function `deneme.foo`
>>
>> But that operator is used only when we are about to throw an Error, which is supposed to imply we shouldn't continue with the program anyway. So, just one little GC allocation wouldn't hurt, right? :)
>>
>
> ...but what if the GC is totally disabled or not even initialized? This might be the environment you'd want to use @nogc.

If the GC is totally disabled (like in BetterC), the expression `"Invalid " ~ s` will not even compile in the first place. So you would need to create your error message some other way (perhaps with malloc).
September 04, 2022
On Sunday, 4 September 2022 at 14:54:52 UTC, Paul Backus wrote:
>
> If the GC is totally disabled (like in BetterC), the expression `"Invalid " ~ s` will not even compile in the first place. So you would need to create your error message some other way (perhaps with malloc).

Which make me wonder if the @nogc attribute is totally unnecessary. What if we are using reference counting which only needs malloc/free, then it might be acceptable. Wouldn't it be better if we get an error like GCBohem.Allocate symbol isn't found, the reason would be obvious.

Then you might ban GC from specific functions which perhaps is you want. However, suddenly we have fringe desires like in this thread.
September 05, 2022

On Sunday, 4 September 2022 at 15:05:45 UTC, IGotD- wrote:

>

Which make me wonder if the @nogc attribute is totally unnecessary.
...
Wouldn't it be better if we get an error like GCBohem.Allocate symbol isn't found, the reason would be obvious.

One of the main motivators for avoiding the GC is to prevent low latency code from suffering the GC's "stop the world" pauses.

By operating at the function level, rather than the whole program level, @nogc can help write correct programs where latency tolerant code can use the GC, and low latency code can avoid it by running in separate threads that don't get stopped durring collections. Simply removing the GC entirely from a program forces everything to be @nogc, instead of just the most latency sensitive parts.

(Having said that, @nogc is a very incomplete and unsafe solution to this problem, since it doesn't make any attempt to prevent GC-using parts of the program from passing ownership of GC memory to @nogc threads through parameters or shared memory, nor does it prevent the use of modules with GC dependant module thread-local constructors.)

September 05, 2022
First of all, I messed up the subject line. I didn't mean "implicit @nogc" but "@nogc should still allow allocation" for contract strings.

On 9/4/22 07:06, Ali Çehreli wrote:

> we are about to throw an Error,
> which is supposed to imply we shouldn't continue with the program
> anyway.

I went ahead and over-engineered a solution which is probably broken in some important way but I am happy with it. :)

The trick is, instead of making an error string right there and then, an Error is thrown, which contains some data. The GC is used later when error data is being used. Good luck if data has a copy constructor which needs the GC. :p

This is a best-effort mechanism because there is a pre-thread instance for that specific Error, which carries error data.

Here is how it works:

The programmer defines an Error with a distinguishing string tag. The SumType definition must list all kinds of data that such an error can carry. (Luckily, a compile-time error instructs the programmer about the missing Tuple types that need to be added.)

mixin NoGcError!("Foo",
                 SumType!(Tuple!(Foo, int, int),
                          Tuple!(string),
                          Tuple!()));

Although "Foo" could be anything, it makes sense to associate it e.g. with a struct:

struct Foo {
  int i;


The user must specify "Foo" when producing the error string:

  void bar(int i, int j) @safe pure nothrow @nogc
  in (i == 42, error!"Foo"("Something is not right", this, i, j)) {
    // ...
  }


Note different type of error data here:

  void zar(string str, double d) @safe pure nothrow @nogc
  in (!isNaN(d), error!"Foo"("Must not be nan", str)) {
    // ...
  }
}

Of course, error!"Foo" can be used to throw outside of contracts as well:

void main() {
  auto f = Foo(1);
  // f.bar(42, 44);
  // f.zar("hello world", double.init);
  error!"Foo"("Wroing!");
}

Here is a complete draft:

```D
import std; // Sorry :(

mixin NoGcError!("Foo",
                 SumType!(Tuple!(Foo, int, int),
                          Tuple!(string),
                          Tuple!()));

struct Foo {
  int i;

  void bar(int i, int j) @safe pure nothrow @nogc
  in (i == 42, error!"Foo"("Something is not right", this, i, j)) {
    // ...
  }

  void zar(string str, double d) @safe pure nothrow @nogc
  in (!isNaN(d), error!"Foo"("Must not be nan", str)) {
    // ...
  }
}

void main() {
  auto f = Foo(1);
  // f.bar(42, 44);
  // f.zar("hello world", double.init);
  error!"Foo"("Wroing!");
}

mixin template NoGcError(string tag, Data) {
  class NoGcError : Error {
    string msg;
    Data data;

    enum noDataString = Tuple!()().to!string;

    this() {
      super(tag ~ " Error");
    }

    // Adapted from object.Throwable.toString
    override
    void toString(scope void delegate(in char[]) sink) const {
      sink(file); sink(":"); sink(line.to!string); sink(": ");
      sink(tag); sink(" Error: "); sink(msg);
      const dataStr = data.to!string;
      if (dataStr != noDataString) {
        sink("\n  Data: "); sink(dataStr);
      }

      if (info) {
        try {
          sink("\n----------------");
          foreach (t; info) {
            sink("\n"); sink(t);
          }

        } catch (Throwable) {
          // ignore more errors
        }
      }
    }
  }

  static ref error_(string t)()
  if (t == tag) {
    static NoGcError err_;
    return err_;
  }

  static this() {
    error_!tag = new NoGcError();
  }
}

string error(string tag, Data...)(string msg,
                                  Data data,
                                  string file = __FILE__,
                                  size_t line = __LINE__) @safe pure nothrow @nogc {

  static auto thrower(string msg, Data data, string file, size_t line) @trusted nothrow @nogc {
    static assert (__traits(compiles, error_!tag.data = tuple(data)),
                   format!`SumType of NoGcError!"%s" must include Tuple!%s`(tag, Data.stringof));

    error_!tag.msg = msg;
    error_!tag.data = tuple(data);
    error_!tag.file = file;
    error_!tag.line = line;
    throw error_!tag;
  }

  // Adapted from std/regex/internal/ir.d
  static assumePureFunction(T)(T t) @trusted pure nothrow @nogc {
    enum attrs = functionAttributes!T | FunctionAttribute.pure_;
    return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t;
  }

  assumePureFunction(&thrower)(msg, data, file, line);

  assert(false);
}
```

Ali

September 12, 2022
On 9/4/22 07:06, Ali Çehreli wrote:
> void foo(string s) @safe pure nothrow @nogc
> in (s != "hello", "Invalid " ~ s) {
> }
>
> void main() {}
>
> Error: cannot use operator `~` in `@nogc` function `deneme.foo`

I have an experimental solution that can throw Error objects from @nogc code:

  https://code.dlang.org/packages/alid%3Aerrornogc

Documentation:

  https://alid.dpldocs.info/v0.1.1/alid.errornogc.NogcError.html

Ali