Skip to content

Take no .init of the element type in ElementType, nor of the range in each - #11143

Draft
ehudt-weka wants to merge 2 commits into
dlang:masterfrom
ehudt-weka:traits-init-answers
Draft

ehudt-weka wants to merge 2 commits into
dlang:masterfrom
ehudt-weka:traits-init-answers

Conversation

@ehudt-weka

Copy link
Copy Markdown

Stacked on #11119: the first commit is #11119's, and this PR's change is the second commit.
This PR holds the replacements of .init that change answers, split out of #11119 as asked in
review there.

Rationale

After #11119, three .init uses remain in the range traits and each:

  • ElementType reads the element type as typeof(lvalueOf!R.front.init), and
    typeof(R.init.front.init) for an enum.
  • isBidirectionalRange compares typeof(lvalueOf!R.back.init) with it.
  • The constraint of each tests its function on R.init.front.

For a struct or a static array, .init builds the type's whole default-initializer literal,
so these grow with the size of the element type, or of the range for each. This PR replaces
them:

  • ElementType and isBidirectionalRange read the element type through two private helpers,
    frontOf and backOf, which are auto ref functions returning r.front and r.back. In a
    return statement a non-@property front() is called; a bare typeof(lvalueOf!R.front)
    would yield the function type instead. A field or ref result is returned by reference, so
    a non-copyable element type still gets an answer.
  • each tests its function on lvalueOf!R.front.

Changed answers

The differential check described in #11119 (629 types, 64,860 answers) finds 210 answers that
differ between #11119 and this PR, in three groups. The changelog entry documents all three.

  1. each with a ref parameter on a forward range whose front is a field now compiles, as
    it already did on arrays and input ranges (3 answers). R.init.front is an rvalue only
    because R.init is a literal; lvalueOf!R.front is an lvalue.

  2. ElementType and ElementEncodingType of an enum whose base type is a non-string array
    (enum E : int[], enum E : int[3], enum E : const(int)[]) are now void (45 answers,
    with the qualifier variants). They were inout(int), a type no value of E produces: a
    variable of E has no front, and isInputRange!E is false on both. foreach over E,
    ForeachType!E (int) and each with an untyped function do not change. Code that names
    ElementType!E, such as the check's each with an immutable ElementType!E parameter, no
    longer compiles.

  3. Where the element type declares a member named init, ElementType was the type of that
    member: void(int v) for a member function, int for a field int init. It is now the
    element type. What builds on ElementType follows (162 answers):

    • An array of such elements is now a random access range with mobile elements, so map
      over it has opIndex.
    • appender!(S[]) now compiles; its element type comes from ElementEncodingType.
    • For S with a field int init, isInputRange!(S[], int) is now false.
    • Code that names ElementType of such an array, such as a function with an
      immutable ElementType!R parameter passed to map, each or minElement, now compiles.

    Where the range type itself declares a member named init, each now accepts it, since
    its constraint no longer reads R.init.front.

Measurements

Same setup and benchmarks as in #11119 (x86_64 dmd 7023163b4, same druntime imports, peak RSS,
3 runs each). Only the rows that change are listed; time does not change beyond run-to-run
noise outside the large types.

Code master #11119 this PR
Trait queries on large types, N = 2^20 0.29 s, 329 MiB 0.09 s, 65 MiB 0.08 s, 49 MiB
Trait queries on large types, N = 2^22 1.34 s, 1135–1168 MiB 0.12 s, 118 MiB 0.10 s, 54 MiB
a.map!(e => e.x).sum, N = 2^22 0.11 s, 137 MiB 0.09 s, 105 MiB 0.07 s, 41 MiB
minElement + maxElement, N = 2^22 1.93 s, 1014–1047 MiB 0.13 s, 234 MiB 0.12 s, 170 MiB
appender!(Huge[]) put, N = 2^22 2.85 s, 765–771 MiB 0.10 s, 84 MiB 0.10 s, 52 MiB

On ordinary code, peak RSS over #11119: Phobos (the dmd perf-check workload) 587–589 MiB
against 587–588 MiB, ctRegex unittests unchanged, and -unittest -o- of 16 Phobos modules
+0.0% to +0.9% (std.range), median +0.2%. The increase comes from the frontOf and backOf
instances, one per inspected type.

Testing

  • New unittests, each failing on master and on Take no .init of the inspected type in traits #11119:
    • each with a ref parameter on a forward range whose front is a field.
    • ElementType and isRandomAccessRange on an array of a struct whose init member comes
      from a mixin template.
    • ElementType of an enum whose base type is int[].
  • make unittest (release, x86_64) on macOS under Rosetta 2, as described in Take no .init of the inspected type in traits #11119: the same
    119 modules pass and the same 10 fail as on master, and all touched modules pass.
  • The differential check.

Pre-review checklist

  • I have performed a self-review of my code.
  • If my PR fixes a bug or introduces a new feature, I have added thorough tests.
  • If my changes are non-trivial and do not concern a reported issue, I have added a changelog entry.

LLM/AI disclosure

This PR was written entirely by an LLM/AI.

An AI coding agent (Claude, by Anthropic) wrote the code, the tests and this description, ran
the checks and measurements in it, and opened this PR at @ehudt-weka's request, from that
account; "I" above is the agent. The self-review checked above is the agent's own (the tests,
the differential check and the measurements), not a human code review.

For a struct or a static array, `T.init` makes the compiler build the
type's whole default-initializer literal, one entry per static-array
element. The range primitives traits, std.traits (CommonType, isIterable,
ForeachType, hasElaborateAssign, the *TypeOf alias-this traits, ...), and
constraints in std.algorithm, std.array and std.typecons used it only to
form an expression of type `T`. They now use lvalueOf and rvalueOf, so
their compile-time memory and time no longer grow with the size of `T`.

CommonType keeps `.init` for operands that are not a struct or union,
since null and array literals merge differently in `?:` than values of
the type do.

Answers are unchanged, except for a type that declares a member named
`init`: `T.init` named that member, so the traits answered for the member
instead of the type.
…in each

ElementType and isBidirectionalRange read the element type as
`typeof(R.init.front.init)` / `typeof(R.init.back.init)`, and each's
constraint tested its function on `R.init.front`. They now use
`frontOf(lvalueOf!R)` / `backOf(lvalueOf!R)`, private functions returning
`r.front` / `r.back`, and `lvalueOf!R.front`, so no default-initializer
literal of the range or element type is built.

This changes three kinds of answers, listed in the changelog: each with a
`ref` parameter on a forward range whose `front` is a field, ElementType
of an enum whose base type is a non-string array, and types that declare
a member named `init`.
@rikkimax

Copy link
Copy Markdown
Contributor

Why are you pursuing this instead of optimising typeof(expr.init)?

@ehudt-weka

Copy link
Copy Markdown
Author

I prototyped that in dmd. When X.init is the whole operand of typeof and .init is the built-in property of a struct or static array, the patched dmd returns an expression of that type without building the literal. It still lays out the struct and checks the field initializers, and a member named init still takes precedence.

It changes no answers: 64,860 trait queries over 629 types give the same output as master, and so do dmd's 3,642 compilable, fail_compilation and runnable files compiled with -o- (except the three -ftime-trace tests, whose output has timestamps). On top of #11119, it matches this PR's compile memory on every large-type benchmark except each, where .init is a call argument, not the operand of typeof (97 MiB against 32 MiB).

It does not replace #11119, though. Only 2 of the 49 uses #11119 replaces have the form typeof(X.init); the rest use .init as a value (R.init.front, foreach (e; T.init), a function argument), where dmd needs the literal itself. With the patched dmd and master Phobos, the trait benchmark on a struct holding a ubyte[1 << 22] still peaks at 1040 MiB, against 118 MiB with #11119.

If you would rather have the dmd change, I can open it and reduce this PR to each.

@rikkimax

Copy link
Copy Markdown
Contributor

Ok, you have established that phobos needs to change.

Although I wouldn't say no to also having the optimisation, but not required.

ehudt-weka added a commit to ehudt-weka/phobos that referenced this pull request Oct 21, 2026
…683364

Brings in the review fixes of dlang#11119 and dlang#11143.

`ifTestable` on a struct or union: when `pred` does not take a `T`, try an
rvalue of each other qualifier of `T`, since the unique literal `T.init`
converted to all of them. A struct with indirections and a `const` or
`immutable` predicate parameter is testable again, as before this change.

Tests for `isIterable`/`ForeachType` (`ref @safe` opApply, non-copyable
range), the range traits without `@property`, a member named `init`, an
enum based on an array, `each` with a `ref` parameter on a field `front`,
`Chunks` over an infinite range in `@safe pure` code, and qualified
`isTuple`. Comments reworded.
@0xEAB 0xEAB added Severity:Enhancement std.traits Review:AI Generated Code that is generated by an LLM AI — or suspected to be. labels Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review:AI Generated Code that is generated by an LLM AI — or suspected to be. Severity:Enhancement std.traits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants