Skip to content

Comments

replace AA runtime hooks with templated implementation#21066

Merged
thewilsonator merged 29 commits intodlang:masterfrom
rainers:tmpl_aa
Aug 6, 2025
Merged

replace AA runtime hooks with templated implementation#21066
thewilsonator merged 29 commits intodlang:masterfrom
rainers:tmpl_aa

Conversation

@rainers
Copy link
Member

@rainers rainers commented Mar 23, 2025

Adds the complete templated implementation to newaa.d, keeping binary compatibility with rt/aaa.d for now.

This should help inferring proper function attributes on the operations, aswell as using regular constructors/copy operation on key and value types.

Four of the six compiler hooks are replaced, but there are two more calls from TypeInfo_AssociativeArray's getHash() and equal() to forward.

Most of the time was spent fighting inout in template arguments :/

@dlang-bot
Copy link
Contributor

dlang-bot commented Mar 23, 2025

Thanks for your pull request, @rainers!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

⚠️⚠️⚠️ Warnings ⚠️⚠️⚠️

  • In preparation for migrating from Bugzilla to GitHub Issues, the issue reference syntax has changed. Please add the word "Bugzilla" to issue references. For example, Fix Bugzilla Issue 12345 or Fix Bugzilla 12345.(Reminder: the edit needs to be done in the Git commit message, not the GitHub pull request.)

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#21066"

@thewilsonator thewilsonator marked this pull request as draft March 23, 2025 21:51
@jmdavis
Copy link
Member

jmdavis commented Mar 24, 2025

Thank you for working on this.

@rainers rainers force-pushed the tmpl_aa branch 3 times, most recently from bb48686 to 9d998e3 Compare March 27, 2025 18:37
@rainers rainers force-pushed the tmpl_aa branch 3 times, most recently from 536a1ca to ea07044 Compare March 30, 2025 07:54
@rainers rainers force-pushed the tmpl_aa branch 3 times, most recently from 8fdf93d to 4521625 Compare April 12, 2025 08:51
@rainers rainers force-pushed the tmpl_aa branch 3 times, most recently from 261f84d to 3a3faac Compare May 24, 2025 08:22
@rainers rainers force-pushed the tmpl_aa branch 5 times, most recently from e31b9fb to e339e39 Compare June 19, 2025 17:26
@rainers rainers force-pushed the tmpl_aa branch 4 times, most recently from f5d5afb to 8705d8a Compare June 23, 2025 05:48
@rainers rainers marked this pull request as ready for review June 24, 2025 05:40
@rainers rainers requested a review from ibuclaw as a code owner June 24, 2025 05:40
@rainers
Copy link
Member Author

rainers commented Jun 24, 2025

This now passes all the tests with all associative array operations templated and the runtime implementation removed. It is probably a bit large for review, though. I have rebased and split off a couple of preliminary commits, some cherry picking
fixes from stable. The main changes in the last couple of commits are still large. Some notes:

  • implemented hooks as templates:

    • _d_assocarrayliteralTX
    • _d_aaNew
    • _d_aaEqual
    • _d_aaIn
    • _d_aaDel
    • _aaLen
    • _aaGetY
    • _aaGetRvalueX
    • _aaApply/_aaApply2
    • TypeInfo_AssociativeArray.equals
    • TypeInfo_AssociativeArray.getHash

    I kept most names as before, but I think it would be better to use the _d_ prefix more consistently.

  • other template functions called from object.d:

    • _aaClear
    • _aaRehash
    • _aaDup
    • _aaValues
    • _aaKeys
    • _aaRange, _aaRangeEmpty, _aaRangeFrontKey, _aaRangeFrontValue, _aaRangePopFront
  • _d_assocarrayliteralTXTrace removed. No other AA operation is hooked for -profile-gc,
    and that function is pretty unreliable anyway.

  • indexing an associative array now lowered to _aaGetRvalueX/_aaGetY+ContructExp/opAssign in the semantic
    phase:

    • implementation details for the respective AST node IndexExp (I guess some of this should be added as documentation to the lowering code):
      • in an assignment/pre-in/dec operations, (multi-dimensional) ArrayExps on left-hand-side are marked modifiable
        before semantic analysis recurses down
      • while ArrayExps are analyzed and converted to IndexExp, transfer modifiable
      • IndexExp with modifiable == false are converted to call to _aaGetRvalueX + optional bounds check
      • IndexExp with modifiable == true are rewritten after both sides of the assignments are analyzed
        with the same (sometimes surprising) semantics as before
      • IndexExp is kept, but is indexing pointer to value, e.g. aa[key] -> _aaGetRvalueX(aa, key)[0]
      • IndexExp.loweredFrom keeps reference to original expression, used for expression printing
      • multi-dimensional modifying access is handled immediately, no need to rewrite later
    • no need to reimplement the semantics in CTFE again, code removed in interpreter
  • AA templates are now in newaa that so far only contained the AA literal generation for static vars.
    newaa is not a good name in the long run, though, should just be aa? assocarray?

  • slicing array now also ok in todt.d (used to support only strings)

  • aa.remove(key) now works with alias this

  • RemoveExp could be removed completely

  • aa[key] = S with S.ctor and S.opAssign no longer a double lookup

  • creating an AA literal at runtime passes keys and values as arguments normally, not with
    special move/blit operations, so more postblit/dtor operations can happen

  • AA no longer available at CTFE without object.d/newaa.d. There was some support for this, but no more. Is this a problem?

  • Some lowerings are no longer shown in error messages, but expression precedence information might be lost lost if operator translated to call.

@rainers
Copy link
Member Author

rainers commented Nov 21, 2025

This PR caused a regression in diagnostic reporting from C++ backends

Not sure what you mean. With the empty object.d I get

fail_compilation\test20863.d(3): Error: `object._d_aaIn` not found. The current runtime does not support key in AA, or the runtime is corrupt.

If you mean a regular build with object.d from druntime: the backend no longer needs to deal with AA operations anymore.

@ibuclaw
Copy link
Member

ibuclaw commented Nov 21, 2025

The ICE is not good, but I don't know if the example should be valid. What is the accepted required support for declaring your own object.d file and stubbing out things?

Correct, it is not meant to be valid. https://gcc.gnu.org/pr100967

@ibuclaw
Copy link
Member

ibuclaw commented Nov 21, 2025

This PR caused a regression in diagnostic reporting from C++ backends

Not sure what you mean. With the empty object.d I get

Before this change:

$ gdc-trunk -fno-rtti rtti1.d
rtti1.d: In function ‘testInExp’:
rtti1.d:7:12: error: expression ‘key in aa’ requires ‘object.TypeInfo’ and cannot be used with ‘-fno-rtti’
    7 |     return key in aa; // { dg-error "requires .object.TypeInfo. and cannot be used with .-fno-rtti." }
      |            ^

After:

core/internal/array/utils.d:154:41: error: ‘object.TypeInfo’ cannot be used with ‘-fno-rtti’
  154 |     auto ptr = GC.malloc(arrSize, attr, typeid(T));
      |                                         ^
core/internal/newaa.d: In function ‘__ctor’:
core/internal/newaa.d:225:19: error: expression ‘typeid(Entry!(int, int))’ requires ‘object.TypeInfo’ and cannot be used with ‘-fno-rtti’
  225 |         entryTI = typeid(Entry!(K, V));
      |                   ^
core/internal/array/utils.d: In function ‘__arrayAlloc’:
core/internal/array/utils.d:154:41: error: expression ‘typeid(Bucket!(int, int))’ requires ‘object.TypeInfo’ and cannot be used with ‘-fno-rtti’
  154 |     auto ptr = GC.malloc(arrSize, attr, typeid(T));
      |                                         ^

The problem seems to be that now the use of RTTI is deep within some template, and it's not possible to get the instantiation trace from the C++ side of the compiler interface.

I can expose this function as extern(C++) and see where it goes from there.

https://github.com/dlang/dmd/blob/master/compiler/src/dmd/dtemplate.d#L962-L963

@rainers
Copy link
Member Author

rainers commented Nov 21, 2025

Ah, ok. entryTI and a couple more fields are only kept for binary compatibility and could be removed altogether, including the typeid-assignment:

 entryTI = typeid(Entry!(K, V));

Would that help? An array of buckets is allocated later, normally using typeid, too. Is this changed for -nortti somehow?

Does gdb know about the specific layout of the AA impl struct or would a change be ok? I think the mago debug engine would need to be adapted if it is changed.

@schveiguy
Copy link
Member

All the AA details should be opaque to the user. We should be able to change them at any time, as long as the public API is the same.

@ibuclaw
Copy link
Member

ibuclaw commented Nov 23, 2025

The test in gdc was for checking that we get a friendly error and no ICE/segfault when compiling with -fno-rtti.

Now the error is incoherent because rtti use is disconnected from the user code that triggered the template instantiation (this is also a good thing as a druntime implementation could eschew rtti).

Templates are the inverse of opaque, if something goes wrong in the details of the template because user code did something that wasn't expected, then it's innards will always be exposed.

@rainers
Copy link
Member Author

rainers commented Nov 24, 2025

@ibuclaw #22137 removes the error, allowing AAs to be used without TypeInfo.

@rainers
Copy link
Member Author

rainers commented Nov 24, 2025

Templates are the inverse of opaque, if something goes wrong in the details of the template because user code did something that wasn't expected, then it's innards will always be exposed.

Maybe we can detect lowerings in printInstantiationTrace() and skip anything between the error and the location of the lowering. These might be stacked aswell, though.

@ibuclaw
Copy link
Member

ibuclaw commented Nov 24, 2025

@rainers - Possibly, though as a primer have added printInstantiationTrace to the C++ interface anyway so that gdc (and possibly ldc) can link errors back to the originating source code. #22139

I think the second part along with what's already done is to adjust the test my end so that it is self contained again - the original tests strictly had no druntime dependencies, even though they didn't have module object; at the top.

// { dg-do compile }
// { dg-options "-fno-rtti" }
// { dg-shouldfail "expressions depend on TypeInfo" }

int* testInExp(int key, int[int] aa)
{
    return key in aa; // { dg-error "requires 'object.TypeInfo' and cannot be used with '-fno-rtti'" }
}

Now they require extra boilerplate added to remain without druntime dependencies.

For example:

// { dg-do compile }
// { dg-options "-fno-rtti" }
// { dg-shouldfail "expressions depend on TypeInfo" }

module object;

class Object {}
class TypeInfo {}

struct AA(K, V)
{
    this(int sz) nothrow
    {
        keyTI = typeid(K); // { dg-error "'object.TypeInfo' cannot be used with '-fno-rtti'" }
    }
    TypeInfo keyTI;
}

auto _d_aaIn(T : V[K], K, V, K2)(inout T a, auto ref scope K2 key)
{
    auto aa = *(cast(inout(AA!(K, V))*)&a); // { dg-note "instantiated from here" }
    return null;
}

int* testInExp(int key, int[int] aa)
{
    return key in aa; // { dg-note "instantiated from here" }
}

Given that many other hooks previously dealt with by the glue/backend have been moved to frontend, I ought to check that other similar assumptions aren't made by other lowerings that would cause ICE/segfault when druntime code is missing or incomplete.

@schveiguy
Copy link
Member

Templates are the inverse of opaque, if something goes wrong in the details of the template because user code did something that wasn't expected, then it's innards will always be exposed.

I didn't mean it that way. I meant that the user shouldn't care how the innards work. So changing the template to remove the setting of entryTI is acceptable.

Obviously, they should compile, or provide a good error message if they don't. Ideally this is solved in the library with static asserts, but if needed the compiler can get involved.

@ibuclaw
Copy link
Member

ibuclaw commented Dec 2, 2025

@rainers looks like this work unearthed a latent bug in noreturn.

https://compiler-explorer.com/z/ovTfzYr11

Frontend passes wrong AST to codegen (not introduced by this or AA related).

@kinke
Copy link
Contributor

kinke commented Dec 8, 2025

I guess this PR caused the following deprecation:

void foo() @safe {
    immutable key = 1;
    int[int] aa;
    aa[key] = 123;
}

=>

Deprecation: using the result of a cast from `immutable(int)` to `int` as an lvalue will become `@system` in a future release

This broke the Phobos unittests for LDC, since LDC uses -de (for both druntime and Phobos, incl. unittests), whereas DMD doesn't (only for druntime AFAICT).

@rainers
Copy link
Member Author

rainers commented Dec 8, 2025

I guess this PR caused the following deprecation:

This boils down to this code causing the same deprecation message:

void foo() @safe
{
    immutable key = 1;
    _x_aaGetY(cast(int)key);
}

void _x_aaGetY(K)(auto ref K key)
{
}

I suspect auto ref selects the ref version ignoring the deprecation, just to stumble over it later. What is the desired behavior? Should it change depending on the deprecation option?

Note that the cast was inserted by previous versions, too, but the ref was taken by the backend, so it was just accepted silently.

I can remove the cast, but that might delay errors into the template instantiations. Let's see how this works out...

@schveiguy
Copy link
Member

Indeed auto ref picks ref for some reason. I don't know how this is valid. I guess casting between mutability does not for some reason turn an expression into an rvalue?

Seems to have been introduced in 2.095.0 Prior to this, it picks non-ref.

Seems like a bug that shouldn't affect this.

Indeed, why is there a cast in the first place? Why are we using auto ref? So many questions...

rainers added a commit to rainers/dmd that referenced this pull request Dec 9, 2025
…able keys being cast to non-immutable.

Avoid adding the cast for implicitly convertable tyypes.
rainers added a commit to rainers/dmd that referenced this pull request Dec 9, 2025
…able keys being cast to non-immutable.

Avoid adding the cast for implicitly convertable expressions.
@rainers
Copy link
Member Author

rainers commented Dec 9, 2025

Why are we using auto ref?

This is to avoid copying the key as much as possible. There are even tests for this in newaa.d and druntime/test/aa/test_aa.d.

Indeed, why is there a cast in the first place?

Without the cast a couple of tests fail. AFAICT these are mostly issues with value range propagation causing the key expression no longer implicitly casting to the key type.

It seems to work if we avoid the cast for conversions that only change the constness: #22208

thewilsonator pushed a commit that referenced this pull request Dec 9, 2025
…keys being cast to non-immutable. (#22208)

Avoid adding the cast for implicitly convertable expressions.
@schveiguy
Copy link
Member

This is to avoid copying the key as much as possible.

Is it just me, or does this sound like a really bad default for basic types? I would not expect to be using ref on int, just seems like a recipe for poor performance.

@rainers
Copy link
Member Author

rainers commented Dec 10, 2025

Is it just me, or does this sound like a really bad default for basic types? I would not expect to be using ref on int, just seems like a recipe for poor performance.

I don't expect that to make much of a difference, especially in this case where the called function is rather elaborate. If the address needs to be calculated anyway and is passed in registers, the dereferencing is just moved into the function. If inlining kicks in, the optimizer might also elide all the differences.

kassane added a commit to floooh/sokol-d that referenced this pull request Dec 19, 2025
break change in AA runtime in dlang/dmd#21066
kassane added a commit to floooh/sokol-d that referenced this pull request Dec 19, 2025
break change in AA runtime in dlang/dmd#21066
CI: drop userdata from wasm/wgpu runner
@CyberShadow
Copy link
Member

CyberShadow commented Jan 6, 2026

This pull request introduced a regression:
#22354

@ibuclaw
Copy link
Member

ibuclaw commented Feb 19, 2026

@rainers I think this broke strict alignment targets (such as SPARC). :-(

FAIL: runnable/foreach5.d   execution test
FAIL: runnable/foreach5.d -shared-libphobos   execution test
FAIL: runnable/inline.d   execution test
FAIL: runnable/inline.d -shared-libphobos   execution test
FAIL: runnable/interpret.d   execution test
FAIL: runnable/interpret.d -shared-libphobos   execution test
FAIL: runnable/issue18919.d   execution test
FAIL: runnable/issue18919.d -shared-libphobos   execution test
FAIL: runnable/staticaa.d   execution test
FAIL: runnable/staticaa.d -shared-libphobos   execution test
FAIL: runnable/test34.d   execution test
FAIL: runnable/test34.d -shared-libphobos   execution test
FAIL: runnable/test42.d   execution test
FAIL: runnable/test42.d -shared-libphobos   execution test
FAIL: runnable/testaa.d   execution test
FAIL: runnable/testaa.d -fPIC   execution test
FAIL: runnable/testaa.d -fPIC -shared-libphobos   execution test
FAIL: runnable/testaa.d -shared-libphobos   execution test
FAIL: runnable/testaa2.d   execution test
FAIL: runnable/testaa2.d -shared-libphobos   execution test
FAIL: runnable/testaa3.d   execution test
FAIL: runnable/testaa3.d -shared-libphobos   execution test
FAIL: runnable/xtest46.d   execution test
FAIL: runnable/xtest46.d -shared-libphobos   execution test
FAIL: runnable/xtest46_gc.d   execution test
FAIL: runnable/xtest46_gc.d -shared-libphobos   execution test

Going to attempt to get a proper backtrace.

Edit:

Thread 2 received signal SIGBUS, Bus error.
[Switching to Thread 1 (LWP 1)]
0x0014f788 in core.internal.newaa.Impl!(int, immutable(char)[]).Impl.length() const (this=...) at /home/ibuclaw/gcc/libphobos/libdruntime/core/internal/newaa.d:272
272                assert(used >= deleted);
(gdb) bt
#0  0x0014f788 in core.internal.newaa.Impl!(int, immutable(char)[]).Impl.length() const (this=...) at /home/ibuclaw/gcc/libphobos/libdruntime/core/internal/newaa.d:272
#1  0x00152684 in core.internal.newaa.AA!(int, immutable(char)[]).AA.empty() const (this=...) at /home/ibuclaw/gcc/libphobos/libdruntime/core/internal/newaa.d:50
#2  0x0011a8cc in core.internal.newaa._aaValues!(int, immutable(char)[])._aaValues(inout(immutable(char)[][int])) (a=...) at /home/ibuclaw/gcc/libphobos/libdruntime/core/internal/newaa.d:688
#3  0x000f5348 in object.values!(immutable(char)[], int).values(inout(immutable(char)[][int])) (aa=...) at /home/ibuclaw/gcc/libphobos/libdruntime/object.d:3412
#4  0x000fbb9c in testaa.test5520() () at runnable/testaa.d:1165
#5  0x000fe248 in D main () at runnable/testaa.d:1381

@ibuclaw
Copy link
Member

ibuclaw commented Feb 19, 2026

@rainers might have found the culprit.

// NOTE: Returns pointer
_d_assocarrayliteralTX!(int, string)
{
        pure nothrow @safe Impl!(int, string)* _d_assocarrayliteralTX(int[] keys, string[] vals)
        {
            // ...
        }
}

// NOTE: ABI of aa parameter is: struct AA{void* ptr;}
values!(string, int)
{
        auto pure nothrow @property @safe string[] values(inout(string[int]) aa)
        {
                return _aaValues(aa);
        }

}

/// !!! BOOM: Passing pointer to function that accepts a struct.
void main()
{
        string[] a = values(_d_assocarrayliteralTX([5], ["hello"]));
        return 0;
}

This "just works" on x86 as the following types:

void* ptr;
struct aa { void* ptr; }

Are passed around identically. This is not true for all ABIs, however. SPARC32 being one of them (they are passed by invisible reference), and I think ARM would be another too.

This did not occur when the glue/codegen generated the call to _d_assocarrayliteralTX, as it correctly converted the pointer to an AA type.

a = values ({.ptr=_d_assocarrayliteralTX (...)});

@ibuclaw
Copy link
Member

ibuclaw commented Feb 19, 2026

@rainers probably best to deal with this in the glue/codegen layer, so that the AssocArrayLiteral visitor returns
{.ptr=aale.lowering} instead of aale.lowering directly.

@rainers
Copy link
Member Author

rainers commented Feb 22, 2026

@rainers probably best to deal with this in the glue/codegen layer, so that the AssocArrayLiteral visitor returns
{.ptr=aale.lowering} instead of aale.lowering directly.

@ibuclaw the dmd glue layer doesn't really know about associative arrays anymore (but to assert on missing lowerings or to swtitch to it for literals), so I assume that it uses void* as a representation, see also TYaarray. I also don't see anything in the original code using a wrapper struct in 05ed167#diff-77d45d18f4adde2459ebdfef916f5f5fc2050101dc7803278121ddf526dbaccaL4212. Could you just use a pointer in GDC aswell?

For a consistent AST it would be better to return V[K] from _d_assocarrayliteralTX and doing the casting in there, but this prohibits its use during CTFE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.