From 28648309410137a9008c4d9274439232f3162b0c Mon Sep 17 00:00:00 2001 From: metagn Date: Fri, 25 Oct 2024 12:26:42 +0300 Subject: [PATCH] implement type bound operation RFC (#24315) closes https://github.com/nim-lang/RFCs/issues/380, fixes #4773, fixes #14729, fixes #16755, fixes #18150, fixes #22984, refs #11167 (only some comments fixed), refs #12620 (needs tiny workaround) The compiler gains a concept of root "nominal" types (i.e. objects, enums, distincts, direct `Foo = ref object`s, generic versions of all of these). Exported top-level routines in the same module as the nominal types that their parameter types derive from (i.e. with `var`/`sink`/`typedesc`/generic constraints) are considered attached to the respective type, as the RFC states. This happens for every argument regardless of placement. When a call is overloaded and overload matching starts, for all arguments in the call that already have a type, we add any operation with the same name in the scope of the root nominal type of each argument (if it exists) to the overload match. This also happens as arguments gradually get typed after every overload match. This restricts the considered overloads to ones attached to the given arguments, as well as preventing `untyped` arguments from being forcefully typed due to unrelated overloads. There are some caveats: * If no overloads with a name are in scope, type bound ops are not triggered, i.e. if `foo` is not declared, `foo(x)` will not consider a type bound op for `x`. * If overloads in scope do not have enough parameters up to the argument which needs its type bound op considered, then type bound ops are also not added. For example, if only `foo()` is in scope, `foo(x)` will not consider a type bound op for `x`. In the cases of "generic interfaces" like `hash`, `$`, `items` etc. this is not really a problem since any code using it will have at least one typed overload imported. For arbitrary versions of these though, as in the test case for #12620, a workaround is to declare a temporary "template" overload that never matches: ```nim # neither have to be exported, just needed for any use of `foo`: type Placeholder = object proc foo(_: Placeholder) = discard ``` I don't know what a "proper" version of this could be, maybe something to do with the new concepts. Possible directions: A limitation with the proposal is that parameters like `a: ref Foo` are not attached to any type, even if `Foo` is nominal. Fixing this for just `ptr`/`ref` would be a special case, parameters like `seq[Foo]` would still not be attached to `Foo`. We could also skip any *structural* type but this could produce more than one nominal type, i.e. `(Foo, Bar)` (not that this is hard to implement, it just might be unexpected). Converters do not use type bound ops, they still need to be in scope to implicitly convert. But maybe they could also participate in the nominal type consideration: if `Generic[T] = distinct T` has a converter to `T`, both `Generic` and `T` can be considered as nominal roots. The other restriction in the proposal, being in the same scope as the nominal type, could maybe be worked around by explicitly attaching to the type, i.e.: `proc foo(x: T) {.attach: T.}`, similar to class extensions in newer OOP languages. The given type `T` needs to be obtainable from the type of the given argument `x` however, i.e. something like `proc foo(x: ref T) {.attach: T.}` doesn't work to fix the `ref` issue since the compiler never obtains `T` from a given `ref T` argument. Edit: Since the module is queried now, this is likely not possible. --------- Co-authored-by: Andreas Rumpf --- changelog.md | 34 +++++++ compiler/options.nim | 1 + compiler/semcall.nim | 63 ++++++++++++- compiler/sigmatch.nim | 25 +++-- compiler/types.nim | 64 +++++++++++++ doc/manual_experimental.md | 111 +++++++++++++++++++++++ tests/config.nims | 1 + tests/sandwich/config.nims | 1 + tests/sandwich/mcontext_thread_local.nim | 15 +++ tests/sandwich/mdollar1.nim | 5 + tests/sandwich/mdollar2.nim | 7 ++ tests/sandwich/mdollar3.nim | 2 + tests/sandwich/mfiles.nim | 11 +++ tests/sandwich/mhandles.nim | 19 ++++ tests/sandwich/mitems.nim | 52 +++++++++++ tests/sandwich/mlistdeques.nim | 35 +++++++ tests/sandwich/mobjhash.nim | 80 ++++++++++++++++ tests/sandwich/mqueuecontainer.nim | 13 +++ tests/sandwich/msetin.nim | 10 ++ tests/sandwich/msetiter1.nim | 7 ++ tests/sandwich/msetiter2.nim | 4 + tests/sandwich/mtasks.nim | 14 +++ tests/sandwich/tcontext_thread_local.nim | 12 +++ tests/sandwich/tdollar.nim | 12 +++ tests/sandwich/tfilehandles.nim | 8 ++ tests/sandwich/titems.nim | 13 +++ tests/sandwich/tobjhash.nim | 59 ++++++++++++ tests/sandwich/tqueuecontainer.nim | 10 ++ tests/sandwich/tsethash.nim | 24 +++++ tests/sandwich/tsetin.nim | 4 + tests/sandwich/tsetiter1.nim | 17 ++++ tests/sandwich/tsetiter2.nim | 9 ++ 32 files changed, 735 insertions(+), 7 deletions(-) create mode 100644 tests/sandwich/config.nims create mode 100644 tests/sandwich/mcontext_thread_local.nim create mode 100644 tests/sandwich/mdollar1.nim create mode 100644 tests/sandwich/mdollar2.nim create mode 100644 tests/sandwich/mdollar3.nim create mode 100644 tests/sandwich/mfiles.nim create mode 100644 tests/sandwich/mhandles.nim create mode 100644 tests/sandwich/mitems.nim create mode 100644 tests/sandwich/mlistdeques.nim create mode 100644 tests/sandwich/mobjhash.nim create mode 100644 tests/sandwich/mqueuecontainer.nim create mode 100644 tests/sandwich/msetin.nim create mode 100644 tests/sandwich/msetiter1.nim create mode 100644 tests/sandwich/msetiter2.nim create mode 100644 tests/sandwich/mtasks.nim create mode 100644 tests/sandwich/tcontext_thread_local.nim create mode 100644 tests/sandwich/tdollar.nim create mode 100644 tests/sandwich/tfilehandles.nim create mode 100644 tests/sandwich/titems.nim create mode 100644 tests/sandwich/tobjhash.nim create mode 100644 tests/sandwich/tqueuecontainer.nim create mode 100644 tests/sandwich/tsethash.nim create mode 100644 tests/sandwich/tsetin.nim create mode 100644 tests/sandwich/tsetiter1.nim create mode 100644 tests/sandwich/tsetiter2.nim 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/options.nim b/compiler/options.nim index b77bdd2a3371..d6f67279774f 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -229,6 +229,7 @@ type # alternative to above: genericsOpenSym vtables + typeBoundOps LegacyFeature* = enum allowSemcheckedAstModification, diff --git a/compiler/semcall.nim b/compiler/semcall.nim index a195e9857f07..fb2f9e97a78e 100644 --- a/compiler/semcall.nim +++ b/compiler/semcall.nim @@ -69,6 +69,39 @@ proc initCandidateSymbols(c: PContext, headSymbol: PNode, result[0].scope, diagnostics) best.state = csNoMatch +proc isAttachableRoutineTo(prc: PSym, arg: PType): bool = + result = false + if arg.owner != prc.owner: return false + for i in 1 ..< prc.typ.len: + if prc.typ.n[i].kind == nkSym and prc.typ.n[i].sym.ast != nil: + # has default value, parameter is not considered in type attachment + continue + let t = nominalRoot(prc.typ[i]) + if t != nil and t.itemId == arg.itemId: + # parameter `i` is a nominal type in this module + # attachable if the nominal root `t` has the same id as `arg` + return true + +proc addTypeBoundSymbols(graph: ModuleGraph, arg: PType, name: PIdent, + filter: TSymKinds, marker: var IntSet, + syms: var seq[tuple[s: PSym, scope: int]]) = + # add type bound ops for `name` based on the argument type `arg` + if arg != nil: + # argument must be typed first, meaning arguments always + # matching `untyped` are ignored + let t = nominalRoot(arg) + if t != nil and t.owner.kind == skModule: + # search module for routines attachable to `t` + let module = t.owner + var iter = default(ModuleIter) + var s = initModuleIter(iter, graph, module, name) + while s != nil: + if s.kind in filter and s.isAttachableRoutineTo(t) and + not containsOrIncl(marker, s.id): + # least priority scope, less than explicit imports: + syms.add((s, -2)) + s = nextModuleIter(iter, graph) + proc pickBestCandidate(c: PContext, headSymbol: PNode, n, orig: PNode, initialBinding: PNode, @@ -88,10 +121,23 @@ proc pickBestCandidate(c: PContext, headSymbol: PNode, best, alt, o, diagnosticsFlag) if len(syms) == 0: return + let allowTypeBoundOps = typeBoundOps in c.features and + # qualified or bound symbols cannot refer to type bound ops + headSymbol.kind in {nkIdent, nkAccQuoted, nkOpenSymChoice, nkOpenSym} + var symMarker = initIntSet() + for s in syms: + symMarker.incl(s.s.id) # current overload being considered var sym = syms[0].s + let name = sym.name var scope = syms[0].scope + if allowTypeBoundOps: + for a in 1 ..< n.len: + # for every already typed argument, add type bound ops + let arg = n[a] + addTypeBoundSymbols(c.graph, arg.typ, name, filter, symMarker, syms) + # starts at 1 because 0 is already done with setup, only needs checking var nextSymIndex = 1 var z: TCandidate # current candidate @@ -106,6 +152,14 @@ proc pickBestCandidate(c: PContext, headSymbol: PNode, # may introduce new symbols with caveats described in recalc branch matches(c, n, orig, z) + if allowTypeBoundOps: + # this match may have given some arguments new types, + # in which case add their type bound ops as well + # type bound ops of arguments always matching `untyped` are not considered + for x in z.newlyTypedOperands: + let arg = n[x] + addTypeBoundSymbols(c.graph, arg.typ, name, filter, symMarker, syms) + if z.state == csMatch: # little hack so that iterators are preferred over everything else: if sym.kind == skIterator: @@ -136,7 +190,14 @@ proc pickBestCandidate(c: PContext, headSymbol: PNode, # before any further candidate init and compare. SLOW, but rare case. syms = initCandidateSymbols(c, headSymbol, initialBinding, filter, best, alt, o, diagnosticsFlag) - + symMarker = initIntSet() + for s in syms: + symMarker.incl(s.s.id) + if allowTypeBoundOps: + for a in 1 ..< n.len: + # for every already typed argument, add type bound ops + let arg = n[a] + addTypeBoundSymbols(c.graph, arg.typ, name, filter, symMarker, syms) # reset counter because syms may be in a new order symCount = c.currentScope.symbols.counter nextSymIndex = 0 diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index 3538ea83cd20..a1a1d21d7f68 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -85,6 +85,9 @@ type inheritancePenalty: int firstMismatch*: MismatchInfo # mismatch info for better error messages diagnosticsEnabled*: bool + newlyTypedOperands*: seq[int] + ## indexes of arguments that are newly typechecked in this match + ## used for type bound op additions TTypeRelFlag* = enum trDontBind @@ -2728,7 +2731,7 @@ proc setSon(father: PNode, at: int, son: PNode) = # father[i] = newNodeIT(nkEmpty, son.info, getSysType(tyVoid)) # we are allowed to modify the calling node in the 'prepare*' procs: -proc prepareOperand(c: PContext; formal: PType; a: PNode): PNode = +proc prepareOperand(c: PContext; formal: PType; a: PNode, newlyTyped: var bool): PNode = if formal.kind == tyUntyped and formal.len != 1: # {tyTypeDesc, tyUntyped, tyTyped, tyError}: # a.typ == nil is valid @@ -2746,15 +2749,17 @@ proc prepareOperand(c: PContext; formal: PType; a: PNode): PNode = #elif formal.kind == tyTyped: {efDetermineType, efWantStmt} #else: {efDetermineType} result = c.semOperand(c, a, flags) + newlyTyped = true else: result = a considerGenSyms(c, result) if result.kind != nkHiddenDeref and result.typ.kind in {tyVar, tyLent} and c.matchedConcept == nil: result = newDeref(result) -proc prepareOperand(c: PContext; a: PNode): PNode = +proc prepareOperand(c: PContext; a: PNode, newlyTyped: var bool): PNode = if a.typ.isNil: result = c.semOperand(c, a, {efDetermineType}) + newlyTyped = true else: result = a considerGenSyms(c, result) @@ -2880,7 +2885,9 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int noMatch() m.baseTypeMatch = false m.typedescMatched = false - n[a][1] = prepareOperand(c, formal.typ, n[a][1]) + var newlyTyped = false + n[a][1] = prepareOperand(c, formal.typ, n[a][1], newlyTyped) + if newlyTyped: m.newlyTypedOperands.add(a) n[a].typ() = n[a][1].typ arg = paramTypesMatch(m, formal.typ, n[a].typ, n[a][1], n[a][1]) @@ -2904,7 +2911,9 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int if tfVarargs in m.callee.flags: # is ok... but don't increment any counters... # we have no formal here to snoop at: - n[a] = prepareOperand(c, n[a]) + var newlyTyped = false + n[a] = prepareOperand(c, n[a], newlyTyped) + if newlyTyped: m.newlyTypedOperands.add(a) if skipTypes(n[a].typ, abstractVar-{tyTypeDesc}).kind==tyString: m.call.add implicitConv(nkHiddenStdConv, getSysType(c.graph, n[a].info, tyCstring), @@ -2918,7 +2927,9 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int m.baseTypeMatch = false m.typedescMatched = false incl(marker, formal.position) - n[a] = prepareOperand(c, formal.typ, n[a]) + var newlyTyped = false + n[a] = prepareOperand(c, formal.typ, n[a], newlyTyped) + if newlyTyped: m.newlyTypedOperands.add(a) arg = paramTypesMatch(m, formal.typ, n[a].typ, n[a], nOrig[a]) if arg != nil and m.baseTypeMatch and container != nil: @@ -2954,7 +2965,9 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int else: m.baseTypeMatch = false m.typedescMatched = false - n[a] = prepareOperand(c, formal.typ, n[a]) + var newlyTyped = false + n[a] = prepareOperand(c, formal.typ, n[a], newlyTyped) + if newlyTyped: m.newlyTypedOperands.add(a) arg = paramTypesMatch(m, formal.typ, n[a].typ, n[a], nOrig[a]) if arg == nil: diff --git a/compiler/types.nim b/compiler/types.nim index fef564f586a0..8ac3ff52f791 100644 --- a/compiler/types.nim +++ b/compiler/types.nim @@ -1935,3 +1935,67 @@ proc isCharArrayPtr*(t: PType; allowPointerToChar: bool): bool = result = false else: result = false + +proc nominalRoot*(t: PType): PType = + ## the "name" type of a given instance of a nominal type, + ## i.e. the type directly associated with the symbol where the root + ## nominal type of `t` was defined, skipping things like generic instances, + ## aliases, `var`/`sink`/`typedesc` modifiers + ## + ## instead of returning the uninstantiated body of a generic type, + ## returns the type of the symbol instead (with tyGenericBody type) + result = nil + case t.kind + of tyAlias, tyVar, tySink: + # varargs? + result = nominalRoot(t.skipModifier) + of tyTypeDesc: + # for proc foo(_: type T) + result = nominalRoot(t.skipModifier) + of tyGenericInvocation, tyGenericInst: + result = t + # skip aliases, so this works in the same module but not in another module: + # type Foo[T] = object + # type Bar[T] = Foo[T] + # proc foo[T](x: Bar[T]) = ... # attached to type + while result.skipModifier.kind in {tyGenericInvocation, tyGenericInst}: + result = result.skipModifier + result = nominalRoot(result[0]) + of tyGenericBody: + result = t + # this time skip the aliases but take the generic body + while result.skipModifier.kind in {tyGenericInvocation, tyGenericInst}: + result = result.skipModifier[0] + let val = result.skipModifier + if val.kind in {tyDistinct, tyEnum, tyObject} or + (val.kind in {tyRef, tyPtr} and tfRefsAnonObj in val.flags): + # atomic nominal types, this generic body is attached to them + discard + else: + result = nominalRoot(val) + of tyCompositeTypeClass: + # parameter with type Foo + result = nominalRoot(t.skipModifier) + of tyGenericParam: + if t.genericParamHasConstraints: + # T: Foo + result = nominalRoot(t.genericConstraint) + else: + result = nil + of tyDistinct, tyEnum, tyObject: + result = t + of tyPtr, tyRef: + if tfRefsAnonObj in t.flags: + # in the case that we have `type Foo = ref object` etc + result = t + else: + # we could allow this in general, but there's things like `seq[Foo]` + #result = nominalRoot(t.skipModifier) + result = nil + of tyStatic: + # ? + result = nil + else: + # skips all typeclasses + # is this correct for `concept`? + result = nil diff --git a/doc/manual_experimental.md b/doc/manual_experimental.md index da51d59ad1ec..9bf64790f5a1 100644 --- a/doc/manual_experimental.md +++ b/doc/manual_experimental.md @@ -2667,3 +2667,114 @@ 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) can have operations attached to it. +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 considered "attached" to 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, +routines with the same name attached to the root nominal type (if it exists) +of each given argument are 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/config.nims b/tests/config.nims index 1862bd40f221..11a427803574 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -43,3 +43,4 @@ when not defined(testsConciseTypeMismatch): switch("legacy", "verboseTypeMismatch") switch("experimental", "vtables") switch("experimental", "openSym") +switch("experimental", "typeBoundOps") diff --git a/tests/sandwich/config.nims b/tests/sandwich/config.nims new file mode 100644 index 000000000000..85506fdcadb6 --- /dev/null +++ b/tests/sandwich/config.nims @@ -0,0 +1 @@ +switch("experimental", "typeBoundOps") diff --git a/tests/sandwich/mcontext_thread_local.nim b/tests/sandwich/mcontext_thread_local.nim new file mode 100644 index 000000000000..718888328d53 --- /dev/null +++ b/tests/sandwich/mcontext_thread_local.nim @@ -0,0 +1,15 @@ +# context_thread_local +import ./mtasks, ./mlistdeques + +export mlistdeques # Exporting the type with destructor doesn't help +# export tasks # solution 1. Exporting the inner type + +type MagicCompile = object + dq: ListDeque[Task] + +# var x: MagicCompile # solution 2. Instantiating the type with destructors +echo "Success" + +type + TLContext* = object + deque*: ListDeque[Task] 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/mfiles.nim b/tests/sandwich/mfiles.nim new file mode 100644 index 000000000000..fb36ecf5a920 --- /dev/null +++ b/tests/sandwich/mfiles.nim @@ -0,0 +1,11 @@ +import mhandles + +type + File* = ref object + handle: Handle[FD] + +proc close*[T: File](f: T) = + f.handle.close() + +proc newFile*(fd: FD): File = + File(handle: initHandle(FD -1)) diff --git a/tests/sandwich/mhandles.nim b/tests/sandwich/mhandles.nim new file mode 100644 index 000000000000..45b143e2c5da --- /dev/null +++ b/tests/sandwich/mhandles.nim @@ -0,0 +1,19 @@ +type + FD* = distinct cint + +type + AnyFD* = concept fd + close(fd) + +proc close*(fd: FD) = + discard + +type + Handle*[T: AnyFD] = object + fd: T + +proc close*[T: AnyFD](h: var Handle[T]) = + close h.fd + +proc initHandle*[T: AnyFD](fd: T): Handle[T] = + Handle[T](fd: fd) diff --git a/tests/sandwich/mitems.nim b/tests/sandwich/mitems.nim new file mode 100644 index 000000000000..ea9d7e7246d7 --- /dev/null +++ b/tests/sandwich/mitems.nim @@ -0,0 +1,52 @@ +import sets, hashes + +type + Fruit* = ref object + id*: int + + # Generic implementation. This doesn't work + EntGroup*[T] = ref object + freed*: HashSet[T] + +proc hash*(self: Fruit): Hash = hash(self.id) + +## +## VVV The Generic implementation. This doesn't work VVV +## + +proc initEntGroup*[T: Fruit](): EntGroup[T] = + result = EntGroup[T]() + result.freed = initHashSet[Fruit]() + var apple = Fruit(id: 20) + result.freed.incl(apple) + +proc get*[T: Fruit](fg: EntGroup[T]): T = + if len(fg.freed) == 0: return + # vvv It errors here + # type mismatch: ([1] fg.freed: HashSet[grouptest.Fruit]) + for it in fg.freed: + return it + +## +## VVV The Non-Generic implementation works VVV +## +type + # Non-generic implementation. This works. + FruitGroup* = ref object + freed*: HashSet[Fruit] + +proc initFruitGroup*(): FruitGroup = + result = FruitGroup() + result.freed = initHashSet[Fruit]() + var apple = Fruit(id: 20) + result.freed.incl(apple) + +proc getNoGeneric*(fg: FruitGroup): Fruit = + if len(fg.freed) == 0: return + for it in fg.freed: + return it + +proc `$`*(self: Fruit): string = + # For echo + if self == nil: return "Fruit()" + return "Fruit(" & $(self.id) & ")" diff --git a/tests/sandwich/mlistdeques.nim b/tests/sandwich/mlistdeques.nim new file mode 100644 index 000000000000..ae18ffaa42fa --- /dev/null +++ b/tests/sandwich/mlistdeques.nim @@ -0,0 +1,35 @@ +# listdeques + +# needed, type bound ops aren't considered for undeclared procs +type Placeholder = object +proc allocate(_: Placeholder) = discard +proc delete(_: Placeholder) = discard + +type + StealableTask* = concept task, var mutTask, type T + task is ptr + task.prev is T + task.next is T + task.parent is T + task.fn is proc (param: pointer) {.nimcall.} + allocate(mutTask) + delete(task) + + ListDeque*[T: StealableTask] = object + head, tail: T + +func isEmpty*(dq: ListDeque): bool {.inline.} = + discard + +func popFirst*[T](dq: var ListDeque[T]): T = + discard + +proc `=destroy`*[T: StealableTask](dq: var ListDeque[T]) = + mixin delete + if dq.isEmpty(): + return + + while (let task = dq.popFirst(); not task.isNil): + delete(task) + + delete(dq.head) diff --git a/tests/sandwich/mobjhash.nim b/tests/sandwich/mobjhash.nim new file mode 100644 index 000000000000..ec342f5f6997 --- /dev/null +++ b/tests/sandwich/mobjhash.nim @@ -0,0 +1,80 @@ +import 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)) + +type + RefObj* = ref object + x*, y*: int + z*: string # to be ignored for equality + +proc `==`*(a, b: RefObj): bool = + a.x == b.x and a.y == b.y + +proc hash*(a: RefObj): Hash = + !$(hash(a.x) !& hash(a.y)) + +type + GenericObj1*[T] = object + x*, y*: T + z*: string # to be ignored for equality + +proc `==`*[T](a, b: GenericObj1[T]): bool = + a.x == b.x and a.y == b.y + +proc hash*[T](a: GenericObj1[T]): Hash = + !$(hash(a.x) !& hash(a.y)) + +type + GenericObj2*[T] = object + x*, y*: T + z*: string # to be ignored for equality + +proc `==`*(a, b: GenericObj2): bool = + a.x == b.x and a.y == b.y + +proc hash*(a: GenericObj2): Hash = + !$(hash(a.x) !& hash(a.y)) + +type + GenericObj3*[T] = object + x*, y*: T + z*: string # to be ignored for equality + GenericObj3Alias*[T] = GenericObj3[T] + +proc `==`*[T](a, b: GenericObj3Alias[T]): bool = + a.x == b.x and a.y == b.y + +proc hash*[T](a: GenericObj3Alias[T]): Hash = + !$(hash(a.x) !& hash(a.y)) + +type + GenericObj4*[T] = object + x*, y*: T + z*: string # to be ignored for equality + GenericObj4Alias*[T] = GenericObj4[T] + +proc `==`*(a, b: GenericObj4): bool = + a.x == b.x and a.y == b.y + +proc hash*(a: GenericObj4): Hash = + !$(hash(a.x) !& hash(a.y)) + +type + GenericRefObj*[T] = ref object + x*, y*: T + z*: string # to be ignored for equality + +proc `==`*[T](a, b: GenericRefObj[T]): bool = + a.x == b.x and a.y == b.y + +proc hash*[T](a: GenericRefObj[T]): Hash = + !$(hash(a.x) !& hash(a.y)) diff --git a/tests/sandwich/mqueuecontainer.nim b/tests/sandwich/mqueuecontainer.nim new file mode 100644 index 000000000000..3ddbc5a3a94b --- /dev/null +++ b/tests/sandwich/mqueuecontainer.nim @@ -0,0 +1,13 @@ +# original example used queues +import deques + +type + QueueContainer*[T] = object + q: ref Deque[T] + +proc init*[T](c: var QueueContainer[T]) = + new(c.q) + c.q[] = initDeque[T](64) + +proc addToQ*[T](c: var QueueContainer[T], item: T) = + c.q[].addLast(item) diff --git a/tests/sandwich/msetin.nim b/tests/sandwich/msetin.nim new file mode 100644 index 000000000000..7aa44df6c554 --- /dev/null +++ b/tests/sandwich/msetin.nim @@ -0,0 +1,10 @@ +import std/sets +template foo*[T](a: T) = +# proc foo*[T](a: T) = # works + var s: HashSet[T] + # echo contains(s, a) # works + let x = a in s # BUG + doAssert not x + doAssert not (a in s) + doAssert a notin s +when isMainModule: foo(1) # works diff --git a/tests/sandwich/msetiter1.nim b/tests/sandwich/msetiter1.nim new file mode 100644 index 000000000000..8049dcf9c848 --- /dev/null +++ b/tests/sandwich/msetiter1.nim @@ -0,0 +1,7 @@ +import sets + +proc initH*[V]: HashSet[V] = + result = initHashSet[V]() + +proc foo*[V](h: var HashSet[V], c: seq[V]) = + h = h + c.toHashSet() diff --git a/tests/sandwich/msetiter2.nim b/tests/sandwich/msetiter2.nim new file mode 100644 index 000000000000..b78175b31089 --- /dev/null +++ b/tests/sandwich/msetiter2.nim @@ -0,0 +1,4 @@ +import sets, sequtils + +proc dedupe*[T](arr: openArray[T]): seq[T] = + arr.toHashSet.toSeq diff --git a/tests/sandwich/mtasks.nim b/tests/sandwich/mtasks.nim new file mode 100644 index 000000000000..f585f4f7b4ff --- /dev/null +++ b/tests/sandwich/mtasks.nim @@ -0,0 +1,14 @@ +# tasks.nim +type + Task* = ptr object + parent*: Task + prev*: Task + next*: Task + fn*: proc (param: pointer) {.nimcall.} + +# StealableTask API +proc allocate*(task: var Task) = + discard + +proc delete*(task: Task) = + discard diff --git a/tests/sandwich/tcontext_thread_local.nim b/tests/sandwich/tcontext_thread_local.nim new file mode 100644 index 000000000000..f015014b8e8f --- /dev/null +++ b/tests/sandwich/tcontext_thread_local.nim @@ -0,0 +1,12 @@ +discard """ + output: ''' +Success +''' +""" + +# modified issue #12620, see placeholder procs in mlistdeques + +# runtime.nim +import ./mcontext_thread_local + +var localCtx* : TLContext 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) diff --git a/tests/sandwich/tfilehandles.nim b/tests/sandwich/tfilehandles.nim new file mode 100644 index 000000000000..3bed18dfae8d --- /dev/null +++ b/tests/sandwich/tfilehandles.nim @@ -0,0 +1,8 @@ +# issue #16755 + +import mfiles +from mhandles import FD +#import handles <- do this and it works + +let wr = newFile(FD -1) +close wr diff --git a/tests/sandwich/titems.nim b/tests/sandwich/titems.nim new file mode 100644 index 000000000000..0c3b41026abb --- /dev/null +++ b/tests/sandwich/titems.nim @@ -0,0 +1,13 @@ +# issue #22984 +# import sets # <<-- Uncomment this to make the error go away + +import mitems + +## The generic implementation +var grp: EntGroup[Fruit] = initEntGroup[Fruit]() +doAssert $get(grp) == "Fruit(20)" ## Errors here + + +## This works though (Non-generic) +var fruitGroup: FruitGroup = initFruitGroup() +doAssert $getNoGeneric(fruitGroup) == "Fruit(20)" diff --git a/tests/sandwich/tobjhash.nim b/tests/sandwich/tobjhash.nim new file mode 100644 index 000000000000..6c82407e773d --- /dev/null +++ b/tests/sandwich/tobjhash.nim @@ -0,0 +1,59 @@ +# https://github.com/nim-lang/RFCs/issues/380 + +from mobjhash import Obj, RefObj, GenericObj1, GenericObj2, GenericObj3, GenericObj4, GenericRefObj +import tables + +block: + var t: Table[Obj, int] + t[Obj(x: 3, y: 4, z: "debug")] = 34 + doAssert t[Obj(x: 3, y: 4, z: "ignored")] == 34 + doAssert Obj(x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[RefObj, int] + t[RefObj(x: 3, y: 4, z: "debug")] = 34 + doAssert t[RefObj(x: 3, y: 4, z: "ignored")] == 34 + doAssert RefObj(x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericObj1[float], int] + t[GenericObj1[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericObj1[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericObj1[float](x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericObj1[int], int] + t[GenericObj1[int](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericObj1[int](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericObj1[int](x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericObj2[float], int] + t[GenericObj2[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericObj2[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericObj2[float](x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericObj3[float], int] + t[GenericObj3[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericObj3[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericObj3[float](x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericObj4[float], int] + t[GenericObj4[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericObj4[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericObj4[float](x: 4, y: 3, z: "debug") notin t + +block: + var t: Table[GenericRefObj[float], int] + t[GenericRefObj[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[GenericRefObj[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert GenericRefObj[float](x: 4, y: 3, z: "debug") notin t + +block: + type LocalAlias[T] = GenericObj4[T] + var t: Table[LocalAlias[float], int] + t[LocalAlias[float](x: 3, y: 4, z: "debug")] = 34 + doAssert t[LocalAlias[float](x: 3, y: 4, z: "ignored")] == 34 + doAssert LocalAlias[float](x: 4, y: 3, z: "debug") notin t diff --git a/tests/sandwich/tqueuecontainer.nim b/tests/sandwich/tqueuecontainer.nim new file mode 100644 index 000000000000..863e44cb7479 --- /dev/null +++ b/tests/sandwich/tqueuecontainer.nim @@ -0,0 +1,10 @@ +# issue #4773 + +import mqueuecontainer + +# works if this is uncommented (or if the `queuecontainer` exports `queues`): +# import queues + +var c: QueueContainer[int] +c.init() +c.addToQ(1) diff --git a/tests/sandwich/tsethash.nim b/tests/sandwich/tsethash.nim new file mode 100644 index 000000000000..ad244d1e66c4 --- /dev/null +++ b/tests/sandwich/tsethash.nim @@ -0,0 +1,24 @@ +# issue #14729 + +import sets, hashes + +type + Iterable[T] = concept x + for value in items(x): + type(value) is T + + Foo[T] = object + t: T + +proc myToSet[T](keys: Iterable[T]): HashSet[T] = + for x in items(keys): result.incl(x) + +proc hash[T](foo: Foo[T]): Hash = + echo "specific hash" + +proc `==`[T](lhs, rhs: Foo[T]): bool = + echo "specific equals" + +let + f = Foo[string](t: "test") + hs = [f, f].myToSet() diff --git a/tests/sandwich/tsetin.nim b/tests/sandwich/tsetin.nim new file mode 100644 index 000000000000..1dd3d2bd9e7c --- /dev/null +++ b/tests/sandwich/tsetin.nim @@ -0,0 +1,4 @@ +# issue #18150 + +import msetin +foo(1) diff --git a/tests/sandwich/tsetiter1.nim b/tests/sandwich/tsetiter1.nim new file mode 100644 index 000000000000..60616c496830 --- /dev/null +++ b/tests/sandwich/tsetiter1.nim @@ -0,0 +1,17 @@ +# comment on issue #11167 + +import hashes + +import msetiter1 + +type + Choice = object + i: int + +proc hash(c: Choice): Hash = + result = Hash(c.i) + +var h = initH[Choice]() +let c = @[Choice(i: 1)] + +foo(h, c) diff --git a/tests/sandwich/tsetiter2.nim b/tests/sandwich/tsetiter2.nim new file mode 100644 index 000000000000..f89eac075f6d --- /dev/null +++ b/tests/sandwich/tsetiter2.nim @@ -0,0 +1,9 @@ +# comment on issue #11167 + +import msetiter2 + +let x = dedupe([1, 2, 3]) +doAssert x.len == 3 +doAssert 1 in x +doAssert 2 in x +doAssert 3 in x