Skip to content

Commit

Permalink
add docs, changelog, clean up PR
Browse files Browse the repository at this point in the history
  • Loading branch information
metagn committed Oct 19, 2024
1 parent a7496e2 commit bc80b16
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 11 deletions.
34 changes: 34 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 0 additions & 4 deletions compiler/nim.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,3 @@ define:useStdoutAsStdmsg
@if nimHasVtables:
experimental:vtables
@end

@if nimHasTypeBoundOps:
experimental:typeBoundOps
@end
1 change: 0 additions & 1 deletion compiler/semexprs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 0 additions & 2 deletions compiler/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions config/nim.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ cc = gcc
hint[LineTooLong]=off
@end

@if nimHasTypeBoundOps:
experimental:typeBoundOps
@end

#hint[XDeclaredButNotUsed]=off

threads:on
Expand Down
110 changes: 110 additions & 0 deletions doc/manual_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
5 changes: 5 additions & 0 deletions tests/sandwich/mdollar1.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type Foo* = object
x*, y*: int

proc `$`*(f: Foo): string =
"Foo(" & $f.x & ", " & $f.y & ")"
7 changes: 7 additions & 0 deletions tests/sandwich/mdollar2.nim
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/sandwich/mdollar3.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
proc debug*[T](obj: T) =
echo "debugging: ", obj # calls generic `$`
12 changes: 12 additions & 0 deletions tests/sandwich/tdollar.nim
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit bc80b16

Please sign in to comment.