Appreciating D

IchorDev zxinsworld at gmail.com
Sat Jul 13 16:33:25 UTC 2024


This is going to be a long one.

I have been thinking about [this blog 
post](https://www.yet-another-blog.com/porting_the_game_to_jai_part0/) a lot recently. It documents how the author—now presumed dead—first used D in 2019 as a replacement to C++ for programming video-games. They discuss how they didn't like the experience of using D, and why they chose to port their code to Jai, an unfinished language which is not available to the general public. They neglect to mention the future implications of the language's unfinished unreleased state for libraryavailability, however.

In the present day it's only nearly two years later, Jai has 
still not materialised a public release despite its seemingly 
avid community, and around half of the problems with D mentioned 
in the post have already been solved. I think this is a great 
indication of the strength of D's community. Despite some recent 
roadblocks in getting new features into the language, it has 
still evolved significantly over the course of a mere two years. 
I'd like to now go over some of the issues the author had with D 
at the time and review them for the sake of appreciating D's 
growth, and analysing some of their misconceptions about the 
language.


> After bouncing between the two available D compilers for 
> Windows (dmd and ldc2) for about 4 years, I find the state of D 
> on Windows to resemble something I would expect from a hobby 
> project, [...]
> From the following problems that I have encountered over the 
> years, by far the biggest one is that debug info on Windows is 
> completely broken:
> [...]
> I was told that “DMD has historically had a lot of issues with 
> [...] debug info” [...] but unfortunately most of [these 
> issues] apply to both compilers, not just dmd.

First of all, GDC is also [available for 
Windows](https://winlibs.com), at least in some capacity.
I cannot speak for the quality of D's compilers on Windows (since 
I have banned it from my house), however I can say that since 
they experienced issues with LDC2 I suspect that a lot of their 
issues with debugging could stem from Visual Studio, or how it 
interfaces with whichever debugger it's using. We're never 
informed whether it was VSCode or the full VS IDE though, so the 
answer remains nebulous.

> - mixins (D’s macro equivalent) generate debug info in a way 
> that causes the debugger to not find the correct file (so you 
> step through disassembly)

The `-mixin` flag has always solved this problem for me, and I 
don't see why it wouldn't work in Windows.

> [...] there are other issues and shortcomings, to a significant 
> part in metaprogramming:
> - different compiler phases interact in weird ways that lead to 
> surprises in metaprogramming, while generating misleading 
> errors [...] like order independent declaration in global 
> scopes sometimes breaking with `mixin`s

I feel that this is to be expected, and this behaviour can be 
easily logically identified with [a bit of 
knowledge](https://dlang.org/spec/intro.html#phases-of-compilation). A compiler is not magical, it cannot know things it has not gone out and discovered yet. Usually D doesn't require forward declaration because it does a pass that alleviates the need for it, but without giving mixins their own pass, they naturally result in sequentially parsed code. D's backend design has a strong preference for shorter compile times, and adding another pass would be a huge detriment to compile times.
D compilers definitely have an error message problem, though. 
Since this post was written many previously confusing error 
messages (especially for missing semicolons & closing 
parenthesis) have gotten a lot better, but you'll always get 
insane error messages from the depths of a fever dream when the 
compiler encounters a fringe error (or possibly even a bug) 
inside of a 7-layer nested mixin from a CTFE lambda inside an 
instantiation of an aliased template.

> or like namespaced names not resolving in mixins unless you use 
> a [special kind of string 
> literal](https://dlang.org/spec/lex.html#token_strings)

This is a very strange one. First of all, D does not really use 
'namespaces', instead we use have scopes. What I *assume* they 
mean is fully qualified identifiers, but I cannot find any 
evidence for anything like this in the history of the D 
specification for mixins or token strings. The other possibility 
is that they're using D's 'namespace' variant of `extern(C++)` 
(which I do not recommend—use the string version instead), since 
they mention C++ interoperability. Perhaps one of those was once 
an implementation detail, but it's also a patently illogical 
idea. Mixins parse strings into a regular D 
expressions/declarations, so why would they ever have a special 
case like this?

> or like `static foreach` not being able to insert multiple 
> `else if`s after an `if`

Here we see a fundamental misunderstanding of D's 
meta-programming. The body of a `static foreach` inlines a series 
of [Declaration 
Statement](https://dlang.org/spec/statement.html#declaration-statement)s. None of D's meta-programming facilities allow you to write incomplete declarations/expressions the way that C's macros do, because of the disastrous effects this has had on C's readability. If you want to write incomplete declarations like having `if` and `else if` declared with meta-programming, use string concatenation with a mixin:
```d
void main(){
	int n;
	import std.writeln;
	mixin((string[] list){
		string ret = "if(n == "~list[0]~"){\nwriteln(`"~list[0]~"`);\n";
		foreach(item; list[1..$]){
			ret ~= "}else if(n == "~item~"){\nwriteln(`"~item~"`);\n";
		}
		return ret ~ "}";
	}(["1", "2", "3"]));
}
```

> ldc2 is awfully slow to compile:
> It takes 1 minute in debug and over 5 minutes in release mode 
> to compile the game. There’s also the issue of ldc2 somehow 
> needing 8GB of RAM in debug and 11.5GB in release mode for 
> compiling a 20MB executable from 2MB of source files, thus 
> causing regular near-death experiences for my laptop.

When hearing about compile times this slow, and using this much 
memory, the first thing that comes to mind is **severe** template 
abuse, which is a compile time bottleneck I've experienced with 
LDC2 before, especially when building for release. What's the 
solution? First of all, acknowledge that whenever you use 
meta-programming, you're making the compiler **run** that code, 
so if that code takes a long time to execute then that will 
necessarily inflate your compile times. No compiler design can 
fix that. A big part of the issue for me was re-instantiating 
template functions in nested `static foreach` loops. Instead, I 
would recommend using CTFE functions that create one big mixin.

> but [ldc2 is] sometimes the only choice because dmd has bugs:
> I’ve encountered a couple of bugs over the years that stopped 
> my program from building, the latest one being this [codegen 
> bug](https://issues.dlang.org/show_bug.cgi?id=23195) when 
> interfacing with C++.

The bug mentioned has been [fixed for some time 
now](https://github.com/dlang/dmd/pull/14651#event-8476344353), 
and importantly, was introduced due to an error in Microsoft's 
own documentation about the calling convention.

> D offers a *betterC* mode that among other things disables 
> garbage collection. However, when using this mode, the standard 
> library does not compile and meta programming is significantly 
> hampered:
> Disabling the garbage collector not only disables it for your 
> compiled code, but also for the code executing at compile time. 
> One major way of metaprogramming in D is injecting new code 
> into the program. That code is a string, and those strings need 
> to be concatenated, and string concatenation uses the garbage 
> collector. So … it just doesn’t compile.

There is quite the errata in this section. Specifically, BetterC 
replaces DRuntime with a wrapper that hooks into C's runtime, 
which makes some basic DRuntime-dependant features like `assert` 
still work. The GC is part of DRuntime, so out it goes. 
Phobos—D's standard library—is *partially* unavailable, with most 
meta-programming facilities (like `std.traits`/`std.meta`) still 
being available.
You might've noticed a discrepancy there. Why would removing the 
garbage collector from the *runtime* stop GC being used at 
*compile-time*? The compiler's interpreter isn't using the same 
runtime as our generated run-time code, right? No, of course it's 
not. D compilers don't even use the GC at compile-time by 
default, although it can be enabled for lower memory consumption. 
What this person has clearly encountered is that run-time 
functions that use the GC/DRuntime naturally don't compile with 
BetterC (or produce linker errors) and so they have naturally 
concluded that CTFE string concatenation is impossible with 
BetterC. This is simply wrong. When you write a run-time function 
(**whether or not** you use it in run-time code) the compiler 
**will** try to generate code for it. There's a way to make 
CTFE-only code-paths in a function, but this feature only works 
with `if`, not `static if`, so it's not useful to us here. What 
you need to do is create a function that the compiler won't try 
to generate run-time code for. You need a lambda:
```d
//not a lambda, will generate run-time code:
string catRT(string a, string b) => a ~ b; //Error: array 
concatenation of expression `a ~ b` requires the GC which is not 
available with -betterC

//a lambda assigned to an enum:
enum catCT = (string a, string b) => a ~ b; //OK
```
Of course, if you call this lambda in run-time code it will 
produce an error (from linking, specifically).

> Bad debug info and the de-facto need for garbage collection are 
> dealbreakers

Garbage collection being a deal-breaker is weird to me as a D 
user, but somewhat understandable for real-time speed-critical 
applications. However, D is a perfectly capable language without 
DRuntime, as you can easily write a custom templated 'array' 
struct that overloads `~` and uses existing C/C++ libraries 
instead of Phobos. If you don't care about executable size, just 
mix GC with manual memory allocation. You'll get to use a lot 
more D libraries that way. Unless you pick a good allocator or 
swap a lot of large long-term GC allocations to manual ones (to 
reduce GC heap scanning) your net time savings will likely be ~0 
anyway.

See this excellent write-up about the matter from Bit Bashing for 
more: [Garbage Collection for Systems 
Programmers](https://bitbashing.io/gc-for-systems-programmers.html)

> **Why is there a single language without the ability to pass 
> parameters by name in 2022??**

The [DIP for named 
parameters](https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1030.md) was accepted back in 2020, so anyone could've seen it coming, but now we've finally got our hands on them! Hurrah!

And that's all! If you like long blog posts about D I recommend 
reading some from [Bit Bashing](https://bitbashing.io), and [The 
Art of Machinery](https://theartofmachinery.com).


More information about the Digitalmars-d mailing list