fixes #4086; allow type-side generic param defaults to reference other type params#25799
fixes #4086; allow type-side generic param defaults to reference other type params#25799puffball1567 wants to merge 4 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 ( |
| # surprising behavior. We must instead fix the expected type of | ||
| # the proc to be the unbound typedesc type: | ||
| # the proc to be the unbound typedesc type. | ||
| # Issue #9355: also handles `proc foo[T](U = T)` where the default |
There was a problem hiding this comment.
What does proc foo[T](U = T) even mean. You cannot give a parameter a default value which is a type, types are not values.
| # extends support to defaults that reference earlier type parameters. | ||
|
|
||
| block: # #9355 sample 1: proc default referencing T (untyped form) | ||
| func foo[T](U = T): U = discard |
There was a problem hiding this comment.
Makes no sense. T is a type, not a value. If you want another generic parameter U, declare it in the brackets.
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. |
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:
type Foo[T; U = seq[T]](also
U = ref T,U = array[3, T], alias, distinct, cascade)type Foo[T; U = T]var f: Foofortype Foo[T = int]func foo[T](U = T): Uandfunc foo[T](U: type = T): UImplementation
compiler/semtypes.nimsemGeneric: accumulate already-resolved generic params into alayered 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 onhasAnyDefaultso generic types with no defaults pay no overhead.semProcTypeNode: treat a bare generic-param default (U = T) asan unbound typedesc parameter, equivalent to
U: type = T, so theexisting typedesc-param machinery handles it.
tryGenericBodyDefaultInvocation: helper that synthesizes aFoo[default1, default2, ...]invocation when every generic paramhas a default, and dispatches to
semGeneric.compiler/semstmts.nimsemVarOrLet/semConst: after type resolution, if the resultingtype is a
tyGenericBodywhose every param has a default, expandit via
tryGenericBodyDefaultInvocation. Restricted to var/let/const so type-level computations like
arity(SomeGeneric)intemplate/typetraits contexts keep treating bare generic-body
references as intended.
compiler/sigmatch.nimmatchesdefault-completion: when the default value of a typedescparam references generic params, substitute via
prepareTypesInBodyand wrap the resolved type as
tyTypeDescso the implicittfImplicitTypeParambinding path treats it like a literal typedefault. Skipped when
nfDefaultRefsParamis set so defaults thatreference value parameters (e.g.
idx = arr.high) keep usingthe existing param-substitution machinery.
Tests
tests/generics/tgeneric_param_default_deps.nimcovers all foursub-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}: 0 new failuresacross 663 tests (one pre-existing
tforwardcycletimeoutreTimeoutreproduces on clean upstream/devel and is unrelated).
closes #4086
closes #9355