# # # 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, semdata, lookups, lineinfos, idents, msgs, renderer, types, layeredtable import std/sets 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.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.. 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 not key.elementType.isNil 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 # Handle self-referential concepts: when a concept references itself in its body # (e.g., `A = concept; proc test(x: Self, y: A)`), the inner type A has n=nil. # We detect this by checking if the concept has the same symbol name as the # one we're currently matching and has no body (n=nil). if concpt.n.isNil: if concpt.sym != nil and m.concpt.sym != nil and concpt.sym == m.concpt.sym: # Self-reference: check if potentialImpl matches what we're already checking return potentialImpl.id == m.potentialImplementation.id # Concept without body that's not a self-reference - cannot match return false # Cycle detection: track (concept, type) pairs to prevent infinite recursion. # Returns true on cycle (coinductive semantics) to support co-dependent concepts. let pair: ConceptTypePair = (concpt.itemId, potentialImpl.itemId) if pair in m.marker: return true m.marker.incl pair var efPot = potentialImpl if potentialImpl.isSelf: if m.concpt.n == concpt.n: m.marker.excl pair 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 result = processConcept(c, concpt, invocation, oldBindings, m) m.potentialImplementation = oldPotentialImplementation m.concpt = oldConcept m.bindings = oldBindings m.marker.excl pair 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 isObjectSubtype(f, a: PType): bool = var t = a result = false while t != nil: t = t.baseClass if t == nil: break t = t.skipTypes({tyPtr,tyRef}) if t == nil: break if t.kind != tyObject: break if sameObjectTypes(f, t): result = true break 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.isSelf: if m.magic in {mArrPut, mArrGet}: return false a = m.potentialImplementation 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) if not result and f.kind == tyObject and a.kind == tyObject: result = isObjectSubtype(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: if f.base.kind == tyNone: result = true elif 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 elif f.kind == tyGenericInvocation: # bind potential generic constraints into body let body = f.base for i in 1 ..< len(f): bindParam(c,m,body[i-1], f[i]) result = matchType(c, body, a, m) else: # tyGenericInst result = matchType(c, f.last, a, m) 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() of tySet: result = false if a.kind == tySet: result = matchType(c, f.elementType, a.elementType, m) 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..= 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)