fixes #4086; allow generic type parameter defaults to reference earlier type params#25799
fixes #4086; allow generic type parameter defaults to reference earlier type params#25799puffball1567 wants to merge 9 commits into
Conversation
…eference other type params
Generic type parameter defaults that reference other type parameters
currently fail in Nim, even though C++ / TypeScript / Rust / Scala all
support the pattern. This change enables four related sub-cases:
1. type-side reference: `type Foo[T; U = seq[T]]`
(also `U = ref T`, `U = array[3, T]`, alias, distinct, cascade)
2. type-side direct: `type Foo[T; U = T]`
3. type-side 0-arg invocation: `var f: Foo` for `type Foo[T = int]`
4. proc-side: `func foo[T](U = T): U` and `func foo[T](U: type = T): U`
Implementation
--------------
compiler/semtypes.nim
- semGeneric: accumulate already-resolved generic params into a
layered type map and substitute them in subsequent default values
before they are added to the invocation. Handles cascade defaults
like `[T; U = seq[T]; V = seq[U]]` left-to-right. Gated on
`hasAnyDefault` so generic types with no defaults pay no overhead.
- semProcTypeNode: treat a bare generic-param default (`U = T`) as
an unbound typedesc parameter, equivalent to `U: type = T`, so the
existing typedesc-param machinery handles it.
- tryGenericBodyDefaultInvocation: helper that synthesizes a
`Foo[default1, default2, ...]` invocation when every generic param
has a default, and dispatches to semGeneric.
compiler/semstmts.nim
- semVarOrLet / semConst: after type resolution, if the resulting
type is a tyGenericBody whose every param has a default, expand
it via tryGenericBodyDefaultInvocation. Restricted to var/let/
const so type-level computations like `arity(SomeGeneric)` in
template/typetraits contexts keep treating bare generic-body
references as intended.
compiler/sigmatch.nim
- matches default-completion: when the default value is a bare
reference to an earlier generic type param (recognised by the
default's static type being `tyGenericParam`), substitute T
against the explicit-instantiation bindings via prepareTypesInBody
and wrap the result as `tyTypeDesc` so the implicit
`tfImplicitTypeParam` binding path treats it like a literal type
default. Defaults whose type is anything else — value expressions
like `arr.high`, proc-call expressions like `newTensor[T](0)` whose
result type is `tyGenericInvocation`/`tyGenericInst`, etc. — are
left untouched, so the existing default-handling machinery (and
later sem stages) continue to handle them as before. Verified by
locally building tests/test_indep_import.nim from arraymancer
against ~/.nimble pkgs2.
Tests
-----
tests/generics/tgeneric_param_default_deps.nim covers all four
sub-cases and the cascade / alias / distinct variants. Local
regression on tests/{generic,generics,objects,typerel,types,
metatype,statictypes,concepts,proc,overload,template,macros,tuples,
collections,distinct,varstmt,let,varres,assign,init}: 663 reSuccess
across 19 categories. The single non-success entry,
tests/types/tforwardcycletimeout reTimeout, reproduces on clean
upstream/devel and is unrelated.
closes nim-lang#4086
closes nim-lang#9355
dade56f to
18f250e
Compare
|
The Azure Pipelines Could a maintainer kick off a re-run of just that one job? cc @Araq @ringabout for review when convenient — generic-param-default fix (4 files, +199/-2). Closes #4086 and #9355. |
|
For 1, 2, 3, we would also need to test this kind of syntax: type Foo[T; U: seq[T]|Deque[T] = seq[T]]
type Foo[T: SomeInteger = int]And probably interation with concepts as well. Note that the fact that higher kinded types actually work is an accident and the original RFC was auto-closed as not planned: nim-lang/RFCs#5 |
…g#9355 Adds three review-requested tests to tgeneric_param_default_deps.nim covering generic param defaults in combination with: 1. union constraint: type Foo[T; U: seq[T]|Deque[T] = seq[T]] 2. typeclass constraint: type Foo[T: SomeInteger = int] 3. concept constraint: type Foo[T: HasLen = string] These exercise the default-completion machinery added in 18f250e across constraint kinds adjacent to higher-kinded-type accidental behavior (RFCs#5). All three pass under both --mm:orc (default) and --mm:refc. Local regression on tests/concepts: 45/46 reSuccess, 0 reFail (1 reDisabled is unrelated). Local regression on tests/generics: 116/116 reSuccess, 0 reFail.
|
@mratsim Thanks. Pushed
All three pass on On HKT (RFCs#5): the patch is scoped to default-completion only. The guards ( |
Per @Araq's review feedback: - `proc foo[T](U = T)` and `proc foo[T](U: type = T)` mix proc-arg defaults with types, which is not valid Nim semantics ("types are not values"). The proper Nim way is brackets-internal generic params: `proc foo[T, U = T]()`. Changes: - compiler/semtypes.nim: revert the `tyGenericParam` OR condition and the associated comment in `semProcTypeNode`; the original `tyTypeDesc`-only logic is preserved. - tests/generics/tgeneric_param_default_deps.nim: replace the proc-arg default samples with brackets-internal forms. The PR core (type-side `[Tp; Alloc = StdAllocator[Tp]]` and proc-side brackets-internal `[T, U = T]` generic param defaults referencing other type params, closing nim-lang#4086 / nim-lang#9355) is preserved.
|
@Araq Thank you for the review and the design feedback. You're right — mixing proc-arg defaults with type references is not valid Nim semantics. I've pushed Changes in this update:
The PR core — type-side Thanks again for the careful review! |
After @Araq's review feedback, the proc-arg type-default pattern was dropped from the compiler logic. The remaining brackets-internal proc-side form (`proc foo[T, U = T]()`) is also out of scope because the PR's substitution machinery targets type bodies, not proc signatures — separate work would be needed for the proc-side path. This commit removes the proc-side test samples and updates the header comment to reflect the type-side-only scope. The PR now closes nim-lang#4086 (type-side `[T; U = seq[T]]` defaults referencing other type params) and leaves nim-lang#9355 (proc-side) for a follow-up change. Tested locally with `nim c --run` under both `--mm:orc` and `--mm:refc`; all remaining blocks pass.
|
Quick follow-up on To match what the patch actually fixes, I removed the proc-side test samples and narrowed the PR scope to type-side default substitution (#4086) only:
Tested locally with both I'll keep #9355 open and tackle the proc-signature path in a separate PR once this lands. Title/description can be updated to reflect the narrower scope when convenient — happy to do that myself if preferred. |
|
If I read things correctly, this doesn't work for parameters.
works but
does not. And it cannot because How about |
Araq pointed out that auto-instantiating a bare `Foo` (for a fully defaulted `type Foo[T = int]`) is inconsistent: it works for `var f: Foo` but cannot work for `proc p(f: Foo)`, because there `Foo` is a type class (implicit generic). That asymmetry would cause bug reports. Switch the explicit-defaults instantiation to `Foo[]` (empty brackets), mirroring C++ `Foo<>`. It is handled in the shared type-resolution path (`semTypeNode`'s nkBracketExpr case), so it works uniformly in every type position -- var/let/const, proc params, return types, fields. A bare `Foo` keeps its type-class meaning everywhere. - semtypes: route empty-bracket `Foo[]` to the default-instantiation helper via new `semGenericOrEmptyBracket`; report a clean error instead of an IndexDefect crash when not every param has a default. - the helper now provides only the first param's default explicitly and lets the matcher complete the rest, so dependent defaults among fully-defaulted params (e.g. `type Foo[T = int; U = seq[T]]; var f: Foo[]`) substitute correctly. - semstmts: drop the bare-`Foo` var/let/const auto-expansion (restores the original semVarOrLet/semConst code). - tests: use `Foo[]`; add cascade and param/return/field position coverage.
…ide scope The typedesc-parameter form `func foo[T](U: type = T)` (issue nim-lang#9355) is resolved by the sigmatch default-substitution that was retained through the scope narrowing; add a regression test for it (default falls back to T, explicit override works) and update the header note to record which proc-side forms are supported vs. rejected by design (untyped `U = T`) / out of scope (`proc foo[T, U = T]()`).
| # Skip the bookkeeping entirely when no param has a default — this | ||
| # generic body cannot need substitution and the work would just be | ||
| # pure overhead (matters for large generic graphs / forward cycles). | ||
| var hasAnyDefault = false |
There was a problem hiding this comment.
But why is this prepass even necessary... You instantiate the type, when the typevar wasn't bound, you see if it had a default and use that, else it's an error. That's the algorithm and Nim seems to never do things properly and instead special cases everything and thus sem.nim grows like a cancer.
| defaultValue.typ.kind != tyTypeDesc: | ||
| let typedescTyp = newTypeS(tyTypeDesc, c, defaultValue.typ) | ||
| typedescTyp.incl tfCheckedForDestructor | ||
| defaultValue.typ = typedescTyp |
There was a problem hiding this comment.
Look, this is proc matches, it is supposed to do overload resolution. It is not its job to compute typevar-bindings per se, it got reused in a creative manner for that purpose. Time to refactor things properly and to undo this creative solution that spiralled into a mess of bullshit.
|
Sorry, I know this is much to ask for but before we tackle more features on top of this silly never ending nightmare of what is ultimately a very simple thing we should refactor things. EDIT: The simple thing is called "unification". |
…ot a prepass/matches Per review: a generic param whose value is a default referencing an earlier param (e.g. type Foo[T; U = seq[T]]) is now resolved during instantiation against the already-bound params, instead of via a prepass in semGeneric or typevar substitution added to proc matches. matches is back to plain overload resolution and the prepass is gone. semtypinst.handleGenericInvocation: the first pass uses a non-erroring typevar lookup, so a bare-param default (U = T) is left for the main left-to-right binding pass instead of failing early. semtypes.semGeneric: a default-filled (nfDefaultParam) param that still mentions earlier params no longer marks the invocation non-concrete; instantiation resolves it. Drop nim-lang#9355 (typedesc-param default) from this PR: it relied on the matches substitution removed here; a proper fix needs separate work in the proc-instantiation path.
The previous commit made the first instantiation pass use a non-erroring type-var lookup for every type-var arg. That changed binding for *outer* type-vars and broke real packages in CI (arraymancer/datamancer and zero_functional failed with 'type mismatch'). Restrict it: outer type-vars keep the original lookupTypeVar path; only a default that references one of this body's own params (e.g. U = T) is treated specially, and only when it is not yet bound (a top-level invocation filled by an earlier arg) is it left for the main left-to-right pass. Own params that are already bound are still processed, so recursive / illegal-recursion handling is unchanged.
semGenericOrEmptyBracket emitted 'every generic parameter must have a default' even when the type is not generic at all (it has no params). Route a non-generic-body empty bracket (e.g. Plain[]) to semGeneric so it reports the proper 'no generic parameters allowed for X'; the 'must have a default' message now only applies to a generic body where some param lacks a default.
|
@Araq — reworked. The I've also taken #9355 back out of this PR — the matches substitution is what it |
Generic type parameter defaults that reference other type parameters currently fail in Nim, even though C++ / TypeScript / Rust / Scala all support the pattern. This makes them work:
type Foo[T; U = seq[T]](alsoU = T,U = ref T,U = array[3, T], cascades like[T; U = seq[T]; V = seq[U]], alias/distinct of the instantiation, and union / typeclass / concept constraints).Foo[](empty brackets) instantiates the type using all of them, e.g.type Foo[T = int]; var f: Foo[]isFoo[int]. It works in every type position (var/let/const, params, return types, fields) and mirrors C++Foo<>.Implementation
A parameter whose value is a default that mentions an earlier parameter is resolved during instantiation, against the already-bound parameters — there is no prepass, and
proc matchesis left to do plain overload resolution:compiler/semtypes.nim(semGeneric): a default-filled parameter (nfDefaultParam) that still mentions earlier params no longer marks the invocation non-concrete, so it reaches instantiation where the value is resolved left-to-right.compiler/semtypinst.nim(handleGenericInvocation): the first pass uses a non-erroring type-var lookup, so a bare-parameter default (U = T) is left for the main left-to-right binding pass rather than failing early.Tests
tests/generics/tgeneric_param_default_deps.nimcovers the variants above on both--mm:orcand--mm:refc.closes #4086