123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- #
- #
- # The Nim Compiler
- # (c) Copyright 2020 Andreas Rumpf
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## New styled concepts for Nim. See https://github.com/nim-lang/RFCs/issues/168
- ## for details. Note this is a first implementation and only the "Concept matching"
- ## section has been implemented.
- import ast, astalgo, semdata, lookups, lineinfos, idents, msgs, renderer, types, layeredtable
- import std/intsets
- when defined(nimPreviewSlimSystem):
- import std/assertions
- const
- logBindings = when defined(debugConcepts): true else: false
- ## Code dealing with Concept declarations
- ## --------------------------------------
- proc declareSelf(c: PContext; info: TLineInfo) =
- ## Adds the magical 'Self' symbols to the current scope.
- let ow = getCurrOwner(c)
- let s = newSym(skType, getIdent(c.cache, "Self"), c.idgen, ow, info)
- s.typ = newType(tyTypeDesc, c.idgen, ow)
- s.typ.flags.incl {tfUnresolved, tfPacked}
- s.typ.add newType(tyEmpty, c.idgen, ow)
- addDecl(c, s, info)
- proc semConceptDecl(c: PContext; n: PNode): PNode =
- ## Recursive helper for semantic checking for the concept declaration.
- ## Currently we only support (possibly empty) lists of statements
- ## containing 'proc' declarations and the like.
- case n.kind
- of nkStmtList, nkStmtListExpr:
- result = shallowCopy(n)
- for i in 0..<n.len:
- result[i] = semConceptDecl(c, n[i])
- of nkProcDef..nkIteratorDef, nkFuncDef:
- result = c.semExpr(c, n, {efWantStmt})
- of nkTypeClassTy:
- result = shallowCopy(n)
- for i in 0..<n.len-1:
- result[i] = n[i]
- result[^1] = semConceptDecl(c, n[^1])
- of nkCommentStmt:
- result = n
- else:
- localError(c.config, n.info, "unexpected construct in the new-styled concept: " & renderTree(n))
- result = n
- proc semConceptDeclaration*(c: PContext; n: PNode): PNode =
- ## Semantic checking for the concept declaration. Runs
- ## when we process the concept itself, not its matching process.
- assert n.kind == nkTypeClassTy
- inc c.inConceptDecl
- openScope(c)
- declareSelf(c, n.info)
- result = semConceptDecl(c, n)
- rawCloseScope(c)
- dec c.inConceptDecl
- ## Concept matching
- ## ----------------
- type
- MatchFlags* = enum
- mfDontBind # Do not bind generic parameters
- mfCheckGeneric # formal <- formal comparison as opposed to formal <- operand
-
- MatchCon = object ## Context we pass around during concept matching.
- bindings: LayeredIdTable
- marker: IntSet ## Some protection against wild runaway recursions.
- potentialImplementation: PType ## the concrete type that might match the concept we try to match.
- magic: TMagic ## mArrGet and mArrPut is wrong in system.nim and
- ## cannot be fixed that easily.
- ## Thus we special case it here.
- concpt: PType ## current concept being evaluated
- depthCount = 0
- flags: set[MatchFlags]
-
- MatchKind = enum
- mkNoMatch, mkSubset, mkSame
- const
- asymmetricConceptParamMods = {tyVar, tySink, tyLent, tyOwned, tyAlias, tyInferred} # param modifiers that to not have to match implementation -> concept
- bindableTypes = {tyGenericParam, tyOr, tyTypeDesc}
- proc conceptMatchNode(c: PContext; n: PNode; m: var MatchCon): bool
- proc matchType(c: PContext; fo, ao: PType; m: var MatchCon): bool
- proc matchReturnType(c: PContext; f, a: PType; m: var MatchCon): bool
- proc processConcept(c: PContext; concpt, invocation: PType, bindings: var LayeredIdTable; m: var MatchCon): bool
- proc existingBinding(m: MatchCon; key: PType): PType =
- ## checks if we bound the type variable 'key' already to some
- ## concrete type.
- result = m.bindings.lookup(key)
- if result == nil:
- result = key
- const
- ignorableForArgType = {tyVar, tySink, tyLent, tyOwned, tyAlias, tyInferred}
- proc unrollGenericParam(param: PType): PType =
- result = param.skipTypes(ignorableForArgType)
- while result.kind in {tyGenericParam, tyTypeDesc} and result.hasElementType and result.elementType.kind != tyNone:
- result = result.elementType
- proc bindParam(c: PContext, m: var MatchCon; key, v: PType): bool {. discardable .} =
- if v.kind == tyTypeDesc:
- return false
- var value = unrollGenericParam(v)
- if value.kind == tyGenericParam:
- value = existingBinding(m, value)
- if value.kind == tyGenericParam:
- if value.hasElementType:
- value = value.elementType
- else:
- return true
- if value.kind == tyStatic:
- return false
- if m.magic in {mArrPut, mArrGet} and value.kind in arrPutGetMagicApplies:
- value = value.last
-
- let old = existingBinding(m, key)
- if old != key:
- # check previously bound value
- if not matchType(c, old, value, m):
- return false
- elif key.hasElementType and key.elementType.kind != tyNone:
- # check constaint
- if matchType(c, unrollGenericParam(key), value, m) == false:
- return false
-
- when logBindings: echo "bind table adding '", key, "', ", value
- assert value != nil
- assert value.kind != tyVoid
- m.bindings.put(key, value)
- return true
- proc defSignatureType(n: PNode): PType = n[0].sym.typ
- proc conceptBody*(n: PType): PNode = n.n.lastSon
- proc acceptsAllTypes(t: PType): bool=
- result = false
- if t.kind == tyAnything:
- result = true
- elif t.kind == tyGenericParam:
- if tfImplicitTypeParam in t.flags:
- result = true
- if not t.hasElementType or t.elementType.kind == tyNone:
- result = true
- proc procDefSignature(s: PSym): PNode {. deprecated .} =
- var nc = s.ast.copyNode()
- for i in 0 .. 5:
- nc.add s.ast[i]
- nc
- proc matchKids(c: PContext; f, a: PType; m: var MatchCon, start=0): bool=
- result = true
- for i in start ..< f.kidsLen - ord(f.kind in {tyGenericInst, tyGenericInvocation}):
- if not matchType(c, f[i], a[i], m): return false
- iterator traverseTyOr(t: PType): PType {. closure .}=
- for i in t.kids:
- case i.kind:
- of tyGenericParam:
- if i.hasElementType:
- for s in traverseTyOr(i.elementType):
- yield s
- else:
- yield i
- else:
- yield i
- proc matchConceptToImpl(c: PContext, f, potentialImpl: PType; m: var MatchCon): bool =
- assert not(potentialImpl.reduceToBase.kind == tyConcept)
- let concpt = f.reduceToBase
- if m.depthCount > 0:
- # concepts that are more then 2 levels deep are treated like
- # tyAnything to stop dependencies from getting out of control
- return true
- var efPot = potentialImpl
- if potentialImpl.isSelf:
- if m.concpt.n == concpt.n:
- return true
- efPot = m.potentialImplementation
-
- var oldBindings = m.bindings
- m.bindings = newTypeMapLayer(m.bindings)
- let oldPotentialImplementation = m.potentialImplementation
- m.potentialImplementation = efPot
- let oldConcept = m.concpt
- m.concpt = concpt
-
- var invocation: PType = nil
- if f.kind in {tyGenericInvocation, tyGenericInst}:
- invocation = f
- inc m.depthCount
- result = processConcept(c, concpt, invocation, oldBindings, m)
- dec m.depthCount
- m.potentialImplementation = oldPotentialImplementation
- m.concpt = oldConcept
- m.bindings = oldBindings
- proc cmpConceptDefs(c: PContext, fn, an: PNode, m: var MatchCon): bool=
- if fn.kind != an.kind:
- return false
- if fn[namePos].sym.name != an[namePos].sym.name:
- return false
- let
- ft = fn.defSignatureType
- at = an.defSignatureType
- if ft.len != at.len:
- return false
-
- for i in 1 ..< ft.n.len:
- m.bindings = m.bindings.newTypeMapLayer()
-
- let aType = at.n[i].typ
- let fType = ft.n[i].typ
-
- if aType.isSelf and fType.isSelf:
- continue
-
- if not matchType(c, fType, aType, m):
- m.bindings.setToPreviousLayer()
- return false
- result = true
- if not matchReturnType(c, ft.returnType, at.returnType, m):
- m.bindings.setToPreviousLayer()
- result = false
- proc conceptsMatch(c: PContext, fc, ac: PType; m: var MatchCon): MatchKind =
- # XXX: In the future this may need extra parameters to carry info for container types
- if fc.n == ac.n:
- # This will have to take generic parameters into account at some point
- return mkSame
- let
- fn = fc.conceptBody
- an = ac.conceptBody
- sameLen = fc.len == ac.len
- var match = false
- for fdef in fn:
- var cmpResult = false
- for ia, ndef in an:
- match = cmpConceptDefs(c, fdef, ndef, m)
- if match:
- break
- if not match:
- return mkNoMatch
- return mkSubset
- proc matchType(c: PContext; fo, ao: PType; m: var MatchCon): bool =
- ## The heart of the concept matching process. 'f' is the formal parameter of some
- ## routine inside the concept that we're looking for. 'a' is the formal parameter
- ## of a routine that might match.
-
- var
- a = ao
- f = fo
-
- if a.kind in bindableTypes:
- a = existingBinding(m, ao)
- if a == ao and a.kind == tyGenericParam and a.hasElementType and a.elementType.kind != tyNone:
- a = a.elementType
-
- if f.isConcept:
- if a.acceptsAllTypes:
- return false
- if a.skipTypes(ignorableForArgType).isConcept:
- # if f is a subset of a then any match to a will also match f. Not the other way around
- return conceptsMatch(c, a.reduceToBase, f.reduceToBase, m) >= mkSubset
- else:
- return matchConceptToImpl(c, f, a, m)
-
- result = false
- case f.kind
- of tyAlias:
- result = matchType(c, f.skipModifier, a, m)
- of tyTypeDesc:
- if isSelf(f):
- let ua = a.skipTypes(asymmetricConceptParamMods)
- if m.magic in {mArrPut, mArrGet}:
- if m.potentialImplementation.reduceToBase.kind in arrPutGetMagicApplies:
- bindParam(c, m, a, last m.potentialImplementation)
- result = true
- #elif ua.isConcept:
- # result = matchType(c, m.concpt, ua, m)
- else:
- result = matchType(c, a.skipTypes(ignorableForArgType), m.potentialImplementation, m)
- else:
- if a.kind == tyTypeDesc:
- if not(a.hasElementType) or a.elementType.kind == tyNone:
- result = true
- elif f.hasElementType:
- result = matchType(c, f.elementType, a.elementType, m)
- of tyVar, tySink, tyLent, tyOwned:
- # modifiers in the concept must be there in the actual implementation
- # too but not vice versa.
- if a.kind == f.kind:
- result = matchType(c, f.elementType, a.elementType, m)
- elif m.magic == mArrPut:
- result = matchType(c, f.elementType, a, m)
- of tyEnum, tyObject, tyDistinct:
- if a.kind in ignorableForArgType:
- result = matchType(c, f, a.skipTypes(ignorableForArgType), m)
- else:
- if a.kind == tyGenericInst:
- # tyOr does this to generic typeclasses
- result = a.base.sym == f.sym
- else:
- result = sameType(f, a)
- of tyEmpty, tyString, tyCstring, tyPointer, tyNil, tyUntyped, tyTyped, tyVoid:
- result = a.skipTypes(ignorableForArgType).kind == f.kind
- of tyBool, tyChar, tyInt..tyUInt64:
- let ak = a.skipTypes(ignorableForArgType)
- result = ak.kind == f.kind or ak.kind == tyOrdinal or
- (ak.kind == tyGenericParam and ak.hasElementType and ak.elementType.kind == tyOrdinal)
- of tyArray, tyTuple, tyVarargs, tyOpenArray, tyRange, tySequence, tyRef, tyPtr:
- if f.kind == tyArray and f.kidsLen == 3 and a.kind == tyArray:
- # XXX: this is a work-around!
- # system.nim creates these for the magic array typeclass
- result = true
- else:
- let ak = a.skipTypes(ignorableForArgType - {f.kind})
- if ak.kind == f.kind and f.kidsLen == ak.kidsLen:
- result = matchKids(c, f, ak, m)
- of tyGenericInvocation, tyGenericInst:
- result = false
- let ea = a.skipTypes(ignorableForArgType)
- if ea.kind in {tyGenericInst, tyGenericInvocation}:
- var
- k1 = f.kidsLen - ord(f.kind == tyGenericInst)
- k2 = ea.kidsLen - ord(ea.kind == tyGenericInst)
- if sameType(f.genericHead, ea.genericHead) and k1 == k2:
- result = true
- for i in 1 ..< k2:
- if not matchType(c, f[i], ea[i], m):
- result = false
- break
- of tyOrdinal:
- result = isOrdinalType(a, allowEnumWithHoles = false) or a.kind == tyGenericParam
- of tyStatic:
- var scomp = f.base
- if scomp.kind == tyGenericParam:
- if f.base.kidsLen > 0:
- scomp = scomp.base
- if a.kind == tyStatic:
- result = matchType(c, scomp, a.base, m)
- else:
- result = matchType(c, scomp, a, m)
- of tyGenericParam:
- if a.acceptsAllTypes:
- discard bindParam(c, m, f, a)
- result = f.acceptsAllTypes
- else:
- result = bindParam(c, m, f, a)
- of tyAnything:
- result = true
- of tyNot:
- if a.kind == tyNot:
- result = matchType(c, f.elementType, a.elementType, m)
- else:
- m.bindings = m.bindings.newTypeMapLayer()
- result = not matchType(c, f.elementType, a, m)
- m.bindings.setToPreviousLayer()
- of tyAnd:
- m.bindings = m.bindings.newTypeMapLayer()
- result = true
- for ff in traverseTyOr(f):
- let r = matchType(c, ff, a, m)
- if not r:
- m.bindings.setToPreviousLayer()
- result = false
- break
- of tyGenericBody:
- var ak = a
- if a.kind == tyGenericBody:
- ak = last(a)
- result = matchType(c, last(f), ak, m)
- of tyCompositeTypeClass:
- if a.kind == tyCompositeTypeClass:
- result = matchKids(c, f, a, m)
- else:
- result = matchType(c, last(f), a, m)
- of tyBuiltInTypeClass:
- let target = f.genericHead.kind
- result = a.skipTypes(ignorableForArgType).reduceToBase.kind == target
- of tyOr:
- if a.kind == tyOr:
- var covered = 0
- for ff in traverseTyOr(f):
- for aa in traverseTyOr(a):
- m.bindings = m.bindings.newTypeMapLayer()
- let r = matchType(c, ff, aa, m)
- if r:
- inc covered
- break
- m.bindings.setToPreviousLayer()
- result = covered >= a.kidsLen
- else:
- for ff in f.kids:
- m.bindings = m.bindings.newTypeMapLayer()
- result = matchType(c, ff, a, m)
- if result: break # and remember the binding!
- m.bindings.setToPreviousLayer()
- else:
- result = false
- if result and ao.kind == tyGenericParam:
- let bf = if f.isSelf: m.potentialImplementation else: f
- if bindParam(c, m, ao, bf):
- when logBindings: echo " ^ reverse binding"
- proc checkConstraint(c: PContext; f, a: PType; m: var MatchCon): bool =
- result = matchType(c, f, a, m) or matchType(c, a, f, m)
- proc matchReturnType(c: PContext; f, a: PType; m: var MatchCon): bool =
- ## Like 'matchType' but with extra logic dealing with proc return types
- ## which can be nil or the 'void' type.
- if f.isEmptyType:
- result = a.isEmptyType
- elif a == nil:
- result = false
- else:
- result = checkConstraint(c, f, a, m)
- proc matchSym(c: PContext; candidate: PSym, n: PNode; m: var MatchCon): bool =
- ## Checks if 'candidate' matches 'n' from the concept body. 'n' is a nkProcDef
- ## or similar.
- # watch out: only add bindings after a completely successful match.
- m.bindings = m.bindings.newTypeMapLayer()
- let can = candidate.typ.n
- let con = defSignatureType(n).n
- if can.len < con.len:
- # too few arguments, cannot be a match:
- return false
-
- if can.len > con.len:
- # too many arguments (not optional)
- for i in con.len ..< can.len:
- if can[i].sym.ast == nil:
- return false
-
- when defined(debugConcepts):
- echo "considering: ", renderTree(candidate.procDefSignature), " ", candidate.magic
-
- let common = min(can.len, con.len)
- for i in 1 ..< common:
- if not checkConstraint(c, con[i].typ, can[i].typ, m):
- m.bindings.setToPreviousLayer()
- return false
-
- if not matchReturnType(c, n.defSignatureType.returnType, candidate.typ.returnType, m):
- m.bindings.setToPreviousLayer()
- return false
- # all other parameters have to be optional parameters:
- for i in common ..< can.len:
- assert can[i].kind == nkSym
- if can[i].sym.ast == nil:
- # has too many arguments one of which is not optional:
- m.bindings.setToPreviousLayer()
- return false
- return true
- proc matchSyms(c: PContext, n: PNode; kinds: set[TSymKind]; m: var MatchCon): bool =
- ## Walk the current scope, extract candidates which the same name as 'n[namePos]',
- ## 'n' is the nkProcDef or similar from the concept that we try to match.
- result = false
- var candidates = searchScopes(c, n[namePos].sym.name, kinds)
- searchImportsAll(c, n[namePos].sym.name, kinds, candidates)
- for candidate in candidates:
- m.magic = candidate.magic
- if matchSym(c, candidate, n, m):
- result = true
- break
- proc conceptMatchNode(c: PContext; n: PNode; m: var MatchCon): bool =
- ## Traverse the concept's AST ('n') and see if every declaration inside 'n'
- ## can be matched with the current scope.
- case n.kind
- of nkStmtList, nkStmtListExpr:
- for i in 0..<n.len:
- if not conceptMatchNode(c, n[i], m):
- return false
- return true
- of nkProcDef, nkFuncDef:
- # procs match any of: proc, template, macro, func, method, converter.
- # The others are more specific.
- # XXX: Enforce .noSideEffect for 'nkFuncDef'? But then what are the use cases...
- const filter = {skProc, skTemplate, skMacro, skFunc, skMethod, skConverter}
- result = matchSyms(c, n, filter, m)
- of nkTemplateDef:
- result = matchSyms(c, n, {skTemplate}, m)
- of nkMacroDef:
- result = matchSyms(c, n, {skMacro}, m)
- of nkConverterDef:
- result = matchSyms(c, n, {skConverter}, m)
- of nkMethodDef:
- result = matchSyms(c, n, {skMethod}, m)
- of nkIteratorDef:
- result = matchSyms(c, n, {skIterator}, m)
- of nkCommentStmt:
- result = true
- else:
- # error was reported earlier.
- result = false
- proc fixBindings(bindings: var LayeredIdTable; concpt: PType; invocation: PType; m: var MatchCon) =
- # invocation != nil means we have a non-atomic concept:
- if invocation != nil and invocation.kind == tyGenericInvocation:
- assert concpt.sym.typ.kind == tyGenericBody
-
- for i in 0 .. concpt.sym.typ.len - 1:
- let thisSym = concpt.sym.typ[i]
- if lookup(bindings, thisSym) != nil:
- # dont trust the bindings over existing ones
- continue
- let found = m.bindings.lookup(thisSym)
- if found != nil:
- when logBindings: echo "Invocation bind: ", thisSym, " ", found
- bindings.put(thisSym, found)
-
- # bind even more generic parameters
- let genBody = invocation.base
- assert genBody.kind == tyGenericBody
- for i in FirstGenericParamAt ..< invocation.kidsLen:
- let bpram = genBody[i - 1]
- if lookup(bindings, invocation[i]) != nil:
- # dont trust the bindings over existing ones
- continue
- let boundV = lookup(bindings, bpram)
- when logBindings: echo "generic body bind: '", invocation[i], "' '", boundV, "'"
- if boundV != nil:
- bindings.put(invocation[i], boundV)
- bindings.put(concpt, m.potentialImplementation)
- proc processConcept(c: PContext; concpt, invocation: PType, bindings: var LayeredIdTable; m: var MatchCon): bool =
- m.bindings = m.bindings.newTypeMapLayer()
- if invocation != nil and invocation.kind == tyGenericInst:
- let genericBody = invocation.base
- for i in 1..<invocation.kidsLen-1:
- # instGenericContainer can bind `tyVoid`
- if invocation[i].kind != tyVoid:
- bindParam(c, m, genericBody[i-1], invocation[i])
- result = conceptMatchNode(c, concpt.conceptBody, m)
- if result and mfDontBind notin m.flags:
- fixBindings(bindings, concpt, invocation, m)
- proc conceptMatch*(c: PContext; concpt, arg: PType; bindings: var LayeredIdTable; invocation: PType, flags: set[MatchFlags] = {}): bool =
- ## Entry point from sigmatch. 'concpt' is the concept we try to match (here still a PType but
- ## we extract its AST via 'concpt.n.lastSon'). 'arg' is the type that might fulfill the
- ## concept's requirements. If so, we return true and fill the 'bindings' with pairs of
- ## (typeVar, instance) pairs. ('typeVar' is usually simply written as a generic 'T'.)
- ## 'invocation' can be nil for atomic concepts. For non-atomic concepts, it contains the
- ## `C[S, T]` parent type that we look for. We need this because we need to store bindings
- ## for 'S' and 'T' inside 'bindings' on a successful match. It is very important that
- ## we do not add any bindings at all on an unsuccessful match!
- var m = MatchCon(bindings: bindings, potentialImplementation: arg, concpt: concpt, flags: flags)
- if arg.isConcept:
- result = conceptsMatch(c, concpt.reduceToBase, arg.reduceToBase, m) >= mkSubset
- elif arg.acceptsAllTypes:
- # XXX: I think this is wrong, or at least partially wrong. Can still test ambiguous types
- result = false
- elif mfCheckGeneric in m.flags:
- # prioritize concepts the least. Specifically if the arg is not a catch all as per above
- result = true
- else:
- result = processConcept(c, concpt, invocation, bindings, m)
-
|