diff --git a/changelog.md b/changelog.md index 1310b6ae3f9f..faef6888f495 100644 --- a/changelog.md +++ b/changelog.md @@ -24,6 +24,40 @@ rounding guarantees (via the ## Language changes +- An experimental option `--experimental:typeBoundOps` has been added that + implements the RFC https://github.com/nim-lang/RFCs/issues/380. + This makes the behavior of interfaces like `hash`, `$`, `==` etc. more + reliable for nominal types across indirect/restricted imports. + + ```nim + # objs.nim + import std/hashes + + type + Obj* = object + x*, y*: int + z*: string # to be ignored for equality + + proc `==`*(a, b: Obj): bool = + a.x == b.x and a.y == b.y + + proc hash*(a: Obj): Hash = + $!(hash(a.x) &! hash(a.y)) + ``` + + ```nim + # main.nim + {.experimental: "typeBoundOps".} + from objs import Obj # objs.hash, objs.`==` not imported + import std/tables + + var t: Table[Obj, int] + t[Obj(x: 3, y: 4, z: "debug")] = 34 + echo t[Obj(x: 3, y: 4, z: "ignored")] # 34 + ``` + + See the [experimental manual](https://nim-lang.github.io/Nim/manual_experimental.html#typeminusbound-overloads) + for more information. ## Compiler changes diff --git a/compiler/nim.cfg b/compiler/nim.cfg index e0a2c65be04d..87c35a711084 100644 --- a/compiler/nim.cfg +++ b/compiler/nim.cfg @@ -60,7 +60,3 @@ define:useStdoutAsStdmsg @if nimHasVtables: experimental:vtables @end - -@if nimHasTypeBoundOps: - experimental:typeBoundOps -@end diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index ce1f7383fa2e..a4f4c5927a83 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -1090,7 +1090,6 @@ proc semOverloadedCallAnalyseEffects(c: PContext, n: PNode, nOrig: PNode, elif callee.kind == skIterator: if efWantIterable in flags: let typ = newTypeS(tyIterable, c) - typ.flags.incl tfCheckedForDestructor rawAddSon(typ, result.typ) result.typ() = typ diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 098f16ce64a4..870b51512186 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -448,8 +448,6 @@ proc semArray(c: PContext, n: PNode, prev: PType): PType = proc semIterableType(c: PContext, n: PNode, prev: PType): PType = result = newOrPrevType(tyIterable, prev, c) - if result.kind == tyIterable: - result.flags.incl tfCheckedForDestructor if n.len == 2: let base = semTypeNode(c, n[1], nil) addSonSkipIntLit(result, base, c.idgen) diff --git a/config/nim.cfg b/config/nim.cfg index e0b164ea68c3..7c99581396df 100644 --- a/config/nim.cfg +++ b/config/nim.cfg @@ -18,10 +18,6 @@ cc = gcc hint[LineTooLong]=off @end -@if nimHasTypeBoundOps: - experimental:typeBoundOps -@end - #hint[XDeclaredButNotUsed]=off threads:on diff --git a/doc/manual_experimental.md b/doc/manual_experimental.md index da51d59ad1ec..af7339cdd3ba 100644 --- a/doc/manual_experimental.md +++ b/doc/manual_experimental.md @@ -2667,3 +2667,113 @@ proc nothing() = ``` The current C(C++) backend implementation cannot generate code for gcc and for vcc at the same time. For example, `{.asmSyntax: "vcc".}` with the ICC compiler will not generate code with intel asm syntax, even though ICC can use both gcc-like and vcc-like asm. + +Type-bound overloads +==================== + +With the experimental option `--experimental:typeBoundOps`, each "root" +nominal type (namely `object`, `enum`, `distinct`, direct `Foo = ref object` +types as well as their generic versions) has an attached scope. +Exported top-level routines declared in the same scope as a nominal type +with a parameter having a type directly deriving from that nominal type (i.e. +with `var`/`sink`/`typedesc` modifiers or being in a generic constraint) +are added to the attached scope of the respective nominal type. +This applies to every parameter regardless of placement. + +When a call to a symbol is openly overloaded and overload matching starts, +for all arguments in the call that have already undergone type checking, +any operation with the same name in the attached scope of the root nominal type +of each argument (if it exists) is added as a candidate to the overload match. +This also happens as arguments gradually get typed after every match to an overload. +This is so that the only overloads considered out of scope are +attached to the types of the given arguments, and that matches to +`untyped` or missing parameters are not influenced by outside overloads. + +If no overloads with a given name are in scope, then overload matching +will not begin, and so type-bound overloads are not considered for that name. +Similarly, if the only overloads with a given name require a parameter to be +`untyped` or missing, then type-bound overloads will not be considered for +the argument in that position. +Generally this means that a "base" overload with a compliant signature should +be in scope so that type-bound overloads can be used. + +In the case of ambiguity between distinct local/imported and type-bound symbols +in overload matching, type-bound symbols are considered as a less specific +scope than imports. + +An example with the `hash` interface in the standard library is as follows: + +```nim +# objs.nim +import std/hashes + +type + Obj* = object + x*, y*: int + z*: string # to be ignored for equality + +proc `==`*(a, b: Obj): bool = + a.x == b.x and a.y == b.y + +proc hash*(a: Obj): Hash = + $!(hash(a.x) &! hash(a.y)) + +# here both `==` and `hash` are attached to Obj +# 1. they are both exported +# 2. they are in the same scope as Obj +# 3. they have parameters with types directly deriving from Obj +# 4. Obj is nominal +``` + +```nim +# main.nim +{.experimental: "typeBoundOps".} +from objs import Obj # objs.hash, objs.`==` not imported +import std/tables +# tables use `hash`, only using the overloads in `std/hashes` and +# the ones in instantiation scope (in this case, there are none) + +var t: Table[Obj, int] +# because tables use `hash` and `==` in a compliant way, +# the overloads bound to Obj are also considered, and in this case match best +t[Obj(x: 3, y: 4, z: "debug")] = 34 +# if `hash` for all objects as in `std/hashes` was used, this would error: +echo t[Obj(x: 3, y: 4, z: "ignored")] # 34 +``` + +Another example, this time with `$` and indirect imports: + +```nim +# foo.nim +type Foo* = object + x*, y*: int + +proc `$`*(f: Foo): string = + "Foo(" & $f.x & ", " & $f.y & ")" +``` + +```nim +# bar.nim +import foo + +proc makeFoo*(x, y: int): Foo = + Foo(x: x, y: y) + +proc useFoo*(f: Foo) = + echo "used: ", f # directly calls `foo.$` from scope + +```nim +# debugger.nim +proc debug*[T](obj: T) = + echo "debugging: ", obj # calls generic `$` +``` + +```nim +# main.nim +{.experimental: "typeBoundOps".} +import bar, debugger # `foo` not imported, so `foo.$` not in scope + +let f = makeFoo(123, 456) +useFoo(f) # used: Foo(123, 456) +debug(f) # debugging: Foo(123, 456) +``` diff --git a/tests/sandwich/mdollar1.nim b/tests/sandwich/mdollar1.nim new file mode 100644 index 000000000000..052bbf0a4005 --- /dev/null +++ b/tests/sandwich/mdollar1.nim @@ -0,0 +1,5 @@ +type Foo* = object + x*, y*: int + +proc `$`*(f: Foo): string = + "Foo(" & $f.x & ", " & $f.y & ")" diff --git a/tests/sandwich/mdollar2.nim b/tests/sandwich/mdollar2.nim new file mode 100644 index 000000000000..97f2da18cb49 --- /dev/null +++ b/tests/sandwich/mdollar2.nim @@ -0,0 +1,7 @@ +import mdollar1 + +proc makeFoo*(x, y: int): Foo = + Foo(x: x, y: y) + +proc useFoo*(f: Foo) = + echo "used: ", f # directly calls `foo.$` from scope diff --git a/tests/sandwich/mdollar3.nim b/tests/sandwich/mdollar3.nim new file mode 100644 index 000000000000..e5e2e4744da4 --- /dev/null +++ b/tests/sandwich/mdollar3.nim @@ -0,0 +1,2 @@ +proc debug*[T](obj: T) = + echo "debugging: ", obj # calls generic `$` diff --git a/tests/sandwich/tdollar.nim b/tests/sandwich/tdollar.nim new file mode 100644 index 000000000000..76cc0723bf3d --- /dev/null +++ b/tests/sandwich/tdollar.nim @@ -0,0 +1,12 @@ +discard """ + output: ''' +used: Foo(123, 456) +debugging: Foo(123, 456) +''' +""" + +import mdollar2, mdollar3 # `mdollar1` not imported, so `mdollar1.$` not in scope + +let f = makeFoo(123, 456) +useFoo(f) # used: Foo(123, 456) +debug(f) # debugging: Foo(123, 456)