Skip to content

Take no .init of the inspected type in traits - #77

Open
ehudt-weka wants to merge 2 commits into
ldc-developers:ldc-v1.38.0-wekafrom
ehudt-weka:init-literal-size-check
Open

ehudt-weka wants to merge 2 commits into
ldc-developers:ldc-v1.38.0-wekafrom
ehudt-weka:init-literal-size-check

Conversation

@ehudt-weka

@ehudt-weka ehudt-weka commented Oct 15, 2026 •

Copy link
Copy Markdown

Companion to weka/ldc#18, which makes a .init an error when its literal costs more than --ctfe-max-alloc. .init of a static array or struct builds a literal with one slot per element and per field even when only its type is used, as in typeof(T.init.field), so a large type can cost gigabytes of compiler memory.

Phobos's traits and common algorithms built T.init of the type they inspect only to name a type or call a member, so under the check they failed on such types. This replaces those uses with lvalueOf/rvalueOf, a null dereference, or a type-only form:

  • std.range.primitives: isInputRange, isForwardRange, isBidirectionalRange, hasSlicing, ElementType.
  • std.traits: CommonType, isIterable, ForeachType, fullyQualifiedName, hasNested, hasElaborateAssign, the alias-this *TypeOf traits, ifTestable.
  • std.typecons: isTuple, the tuple compatibility check, Rebindable, apply.
  • std.algorithm: map, each, substitute, min/max (extremum); std.array: Appender.put.

Three places are not a plain substitution:

  • The range traits add is(typeof(R.sizeof)). R.init used to lay a struct out before the traits' lambdas inferred its members' attributes. Without it, a trait evaluated inside the struct's own declaration infers them impure and @system. An example is Chunks.opSlice, whose return type is typeof(takeExactly(this, 0)).
  • CommonType keeps .init for operands that are not structs or unions, because null and array literals merge differently in ?:.
  • ifTestable keeps .init for the same operands, because a scalar's .init converts by value range (ifTestable!(int, (ubyte a) => a) is true) and a null literal converts to any qualifier. For a struct or union whose pred does not take a T, it tries an rvalue of each qualifier of T instead: the literal T.init is unique, so it converted to all of them, and ifTestable!(S, (immutable S s) => s) stays true for a struct with a pointer field.

So min/max, either and the one-predicate endsWith/findSkip/skipOver on a static-array type whose literal is over the limit still report it.

Answers that change. A differential check, written for the upstream review, asks every changed trait and the algorithms built on them about a zoo of types in 5 qualifiers: 64,860 answers, compiled with LDC 1.38 and stock druntime. 770 differ from the stock Phobos.

712 are on the zoo's 6 types with a member named init (a member function, directly or from a mixin template, a field, or a static factory). Such a member hides the built-in property, so T.init named the member: CommonType!InitField was int for struct InitField { int x; int init; }, and for a struct S with a member function void init(...), ElementType!(S[]) was that function's type and isRandomAccessRange!(S[]) was false. The answers are now about the type itself (InitField, S, true). Compiling our code base (~3,100 files) with each changed site also computing its old answer finds one such answer and no other change.

The other 58, in three groups (counts include the zoo's each call whose parameter is typed ElementType!T, which follows ElementType):

  • ElementType and ElementEncodingType of an enum based on an array (int[], const(int)[], int[3]) are now void; they were inout(int). front accepts such an enum only as its constant-folded literal, and isInputRange is false for it before and after. (45)
  • each with a ref parameter now compiles on a range whose front (or back) is a field. (3)
  • A shared struct with alias this to a static array: ElementType, ElementEncodingType, isIterable and ForeachType depended on what had been asked about the same type before; they now always give the answer it gets when asked first. (10)

Compile cost. Even without the check, these traits no longer build the literals. On 8 of our heaviest modules (same source, stock vs patched druntime and Phobos, macOS, one file per compile): CPU −14% and peak RSS −39% without -unittest (one module went from 23.7 to 4.8 GB); unchanged with -unittest.

Tests (LDC's CMake test targets, macOS arm64)

  • At 4a61203 (head): the Phobos unittest runner, static, release and debug, with every module recompiled, passes all 334 module tests. ctfe: report .init literals costing more than --ctfe-max-alloc WEKAPP-683364 weka/ldc#18's four .init lit tests pass. The differential check above; against 8764193, 64 answers differ, all ifTestable with a const or immutable parameter on a struct with indirections, which now match stock.
  • At 8764193, before the sync with the upstream review: the druntime and Phobos unittest runners fail on the same tests as without this change (environment failures only). The druntime and Phobos modules involved (70 modules) compile with -unittest and LDC's runtime test flags with the same result as without this change (no errors), so the static asserts in their unittests hold. 283 old-form/new-form pairs of the changed expressions, compiled with the stock compiler over nested structs with context pointers, qualified types, @disable this(), @disable this(this), classes, alias this, opApply with a ref @safe delegate and non-copyable elements, give the same answers.
  • ctfe: report .init literals costing more than --ctfe-max-alloc WEKAPP-683364 weka/ldc#18 adds tests/compilable/ctfe_max_alloc_init_phobos.d, which runs these templates on types whose literal is over the limit. Its cases fail with the previous Phobos, except one Chunks case, which fails if the range traits drop R.sizeof.

weka/ldc#18 pins this PR's head as runtime/phobos, so this PR merges first; if the merge rewrites the commits, #18 re-pins. The same change is going upstream as dlang#11119 (traits) and dlang#11143 (ElementType and each); this PR has their review fixes as of 2026-10-21.

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. Its only review so far is the agent's own, not a human code review.

With LDC's `.init` size check (weka/ldc), Phobos templates failed on
types whose `.init` literal is huge: the range traits (`isInputRange`,
`isForwardRange`, `isBidirectionalRange`, `hasSlicing`, `ElementType`),
`CommonType`, `isIterable`, `ForeachType`, `fullyQualifiedName`,
`hasNested`, `hasElaborateAssign`, the alias-this `*TypeOf` traits,
`ifTestable`, `isTuple`, `Rebindable`, `apply`, `map`, `each`,
`substitute`, `min`/`max` (`extremum`) and `Appender.put`. Each built
`T.init` only to name a type or call a member. They now use
`lvalueOf`/`rvalueOf`, a null dereference, or a type-only form, so no
literal is built.

`CommonType` and `ifTestable` keep `.init` for operands that are not
structs or unions: null and array literals merge differently in `?:`,
and a scalar or null literal converts where a function result does not.
The range traits add `is(typeof(R.sizeof))`, which lays out a struct
before its member functions' attributes are inferred, as `R.init` did.

Answers that change: `ifTestable` with a `const` or `immutable`
parameter is false for a struct or union with indirections;
`ElementType` of an enum based on an array is `void` (it was
`inout(int)` for `int[]`, and `isInputRange` is false for such enums);
`each` with a `ref` parameter compiles on a range whose `front` is a
field.
@thewilsonator

Copy link
Copy Markdown

Thanks! please also submit this upstream to dlang/phobos so that it gets picked up in future releases.

@ehudt-weka
ehudt-weka marked this pull request as ready for review October 18, 2026 11:14
@ehudt-weka

Copy link
Copy Markdown
Author

@JohanEngelen this targets your ldc-v1.38.0-weka branch, and weka/ldc#18 pins its head, so it needs to land first. Could you review it? The same change is going upstream as dlang#11119.

…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.
ehudt-weka added a commit to weka/ldc that referenced this pull request Oct 21, 2026
@JohanEngelen

JohanEngelen commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

@ehudt-weka Most of the changes look benign and useful, but the more complex ones need careful review which is very difficult now because it is unclear if the AI added random changes/unittests or whether they are needed and catch actual bugs. The PR description is also tediously long, indicating the same: split this PR into several.
Can you upstream the changes (to dlang)? Then we can cherrypick to weka's 1.38 phobos Edit: Sorry, just saw the upstream PR.

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.

3 participants