Improved support for nkSymChoices in type coercions
For example, this allows you to pick up a proc with a specific signature from an overload set. bugfix: nimrod generated invalid code when a RVO function had a single compile-time param bugfix: nkHiddenDerefs were not neutralised properly for indirect proc calls
This commit is contained in:
parent
7424a60ba1
commit
3c9e3a6a71
5 changed files with 147 additions and 51 deletions
|
|
@ -715,6 +715,7 @@ const
|
||||||
nkCommand, nkCallStrLit}
|
nkCommand, nkCallStrLit}
|
||||||
|
|
||||||
nkLambdaKinds* = {nkLambda, nkDo}
|
nkLambdaKinds* = {nkLambda, nkDo}
|
||||||
|
nkSymChoices* = {nkClosedSymChoice, nkOpenSymChoice}
|
||||||
|
|
||||||
skLocalVars* = {skVar, skLet, skForVar, skParam}
|
skLocalVars* = {skVar, skLet, skForVar, skParam}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@ proc leftAppearsOnRightSide(le, ri: PNode): bool =
|
||||||
proc hasNoInit(call: PNode): bool {.inline.} =
|
proc hasNoInit(call: PNode): bool {.inline.} =
|
||||||
result = call.sons[0].kind == nkSym and sfNoInit in call.sons[0].sym.flags
|
result = call.sons[0].kind == nkSym and sfNoInit in call.sons[0].sym.flags
|
||||||
|
|
||||||
proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc, pl: PRope) =
|
proc fixupCall(p: BProc, le, ri: PNode, d: var TLoc,
|
||||||
var pl = pl
|
callee, params: PRope) =
|
||||||
|
var pl = con(callee, "(".toRope, params)
|
||||||
# getUniqueType() is too expensive here:
|
# getUniqueType() is too expensive here:
|
||||||
var typ = skipTypes(ri.sons[0].typ, abstractInst)
|
var typ = skipTypes(ri.sons[0].typ, abstractInst)
|
||||||
if typ.sons[0] != nil:
|
if typ.sons[0] != nil:
|
||||||
if isInvalidReturnType(typ.sons[0]):
|
if isInvalidReturnType(typ.sons[0]):
|
||||||
if sonsLen(ri) > 1: app(pl, ", ")
|
if params != nil: pl.app(", ")
|
||||||
# beware of 'result = p(result)'. We may need to allocate a temporary:
|
# beware of 'result = p(result)'. We may need to allocate a temporary:
|
||||||
if d.k in {locTemp, locNone} or not leftAppearsOnRightSide(le, ri):
|
if d.k in {locTemp, locNone} or not leftAppearsOnRightSide(le, ri):
|
||||||
# Great, we can use 'd':
|
# Great, we can use 'd':
|
||||||
|
|
@ -125,9 +126,9 @@ proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||||
# getUniqueType() is too expensive here:
|
# getUniqueType() is too expensive here:
|
||||||
var typ = skipTypes(ri.sons[0].typ, abstractInst)
|
var typ = skipTypes(ri.sons[0].typ, abstractInst)
|
||||||
assert(typ.kind == tyProc)
|
assert(typ.kind == tyProc)
|
||||||
|
assert(sonsLen(typ) == sonsLen(typ.n))
|
||||||
var length = sonsLen(ri)
|
var length = sonsLen(ri)
|
||||||
for i in countup(1, length - 1):
|
for i in countup(1, length - 1):
|
||||||
assert(sonsLen(typ) == sonsLen(typ.n))
|
|
||||||
if ri.sons[i].typ.isCompileTimeOnly: continue
|
if ri.sons[i].typ.isCompileTimeOnly: continue
|
||||||
if params != nil: app(params, ", ")
|
if params != nil: app(params, ", ")
|
||||||
if i < sonsLen(typ):
|
if i < sonsLen(typ):
|
||||||
|
|
@ -135,7 +136,7 @@ proc genPrefixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||||
app(params, genArg(p, ri.sons[i], typ.n.sons[i].sym))
|
app(params, genArg(p, ri.sons[i], typ.n.sons[i].sym))
|
||||||
else:
|
else:
|
||||||
app(params, genArgNoParam(p, ri.sons[i]))
|
app(params, genArgNoParam(p, ri.sons[i]))
|
||||||
fixupCall(p, le, ri, d, con(op.r, "(".toRope, params))
|
fixupCall(p, le, ri, d, op.r, params)
|
||||||
|
|
||||||
proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
proc genClosureCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||||
|
|
||||||
|
|
@ -210,16 +211,16 @@ proc genInfixCall(p: BProc, le, ri: PNode, d: var TLoc) =
|
||||||
if skipTypes(param.typ, {tyGenericInst}).kind == tyPtr: app(pl, "->")
|
if skipTypes(param.typ, {tyGenericInst}).kind == tyPtr: app(pl, "->")
|
||||||
else: app(pl, ".")
|
else: app(pl, ".")
|
||||||
app(pl, op.r)
|
app(pl, op.r)
|
||||||
app(pl, "(")
|
var params: PRope
|
||||||
for i in countup(2, length - 1):
|
for i in countup(2, length - 1):
|
||||||
|
if params != nil: params.app(", ")
|
||||||
assert(sonsLen(typ) == sonsLen(typ.n))
|
assert(sonsLen(typ) == sonsLen(typ.n))
|
||||||
if i < sonsLen(typ):
|
if i < sonsLen(typ):
|
||||||
assert(typ.n.sons[i].kind == nkSym)
|
assert(typ.n.sons[i].kind == nkSym)
|
||||||
app(pl, genArg(p, ri.sons[i], typ.n.sons[i].sym))
|
app(params, genArg(p, ri.sons[i], typ.n.sons[i].sym))
|
||||||
else:
|
else:
|
||||||
app(pl, genArgNoParam(p, ri.sons[i]))
|
app(params, genArgNoParam(p, ri.sons[i]))
|
||||||
if i < length - 1: app(pl, ", ")
|
fixupCall(p, le, ri, d, pl, params)
|
||||||
fixupCall(p, le, ri, d, pl)
|
|
||||||
|
|
||||||
proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
|
proc genNamedParamCall(p: BProc, ri: PNode, d: var TLoc) =
|
||||||
# generates a crappy ObjC call
|
# generates a crappy ObjC call
|
||||||
|
|
|
||||||
|
|
@ -125,44 +125,48 @@ proc semSym(c: PContext, n: PNode, s: PSym, flags: TExprFlags): PNode =
|
||||||
else:
|
else:
|
||||||
markUsed(n, s)
|
markUsed(n, s)
|
||||||
result = newSymNode(s, n.info)
|
result = newSymNode(s, n.info)
|
||||||
|
|
||||||
proc checkConversionBetweenObjects(info: TLineInfo, castDest, src: PType) =
|
type
|
||||||
var diff = inheritanceDiff(castDest, src)
|
TConvStatus = enum
|
||||||
if diff == high(int):
|
convOK,
|
||||||
LocalError(info, errGenerated, MsgKindToString(errIllegalConvFromXtoY) % [
|
convNotNeedeed,
|
||||||
src.typeToString, castDest.typeToString])
|
convNotLegal
|
||||||
|
|
||||||
|
proc checkConversionBetweenObjects(castDest, src: PType): TConvStatus =
|
||||||
|
return if inheritanceDiff(castDest, src) == high(int):
|
||||||
|
convNotLegal
|
||||||
|
else:
|
||||||
|
convOK
|
||||||
|
|
||||||
const
|
const
|
||||||
IntegralTypes = {tyBool, tyEnum, tyChar, tyInt..tyUInt64}
|
IntegralTypes = {tyBool, tyEnum, tyChar, tyInt..tyUInt64}
|
||||||
|
|
||||||
proc checkConvertible(info: TLineInfo, castDest, src: PType) =
|
proc checkConvertible(castDest, src: PType): TConvStatus =
|
||||||
if sameType(castDest, src) and castDest.sym == src.sym:
|
result = convOK
|
||||||
|
if sameType(castDest, src) and castDest.sym == src.sym:
|
||||||
# don't annoy conversions that may be needed on another processor:
|
# don't annoy conversions that may be needed on another processor:
|
||||||
if castDest.kind notin IntegralTypes+{tyRange}:
|
if castDest.kind notin IntegralTypes+{tyRange}:
|
||||||
Message(info, hintConvFromXtoItselfNotNeeded, typeToString(castDest))
|
result = convNotNeedeed
|
||||||
return
|
return
|
||||||
var d = skipTypes(castDest, abstractVar)
|
var d = skipTypes(castDest, abstractVar)
|
||||||
var s = skipTypes(src, abstractVar)
|
var s = skipTypes(src, abstractVar)
|
||||||
while (d != nil) and (d.Kind in {tyPtr, tyRef}) and (d.Kind == s.Kind):
|
while (d != nil) and (d.Kind in {tyPtr, tyRef}) and (d.Kind == s.Kind):
|
||||||
d = base(d)
|
d = base(d)
|
||||||
s = base(s)
|
s = base(s)
|
||||||
if d == nil:
|
if d == nil:
|
||||||
LocalError(info, errGenerated, msgKindToString(errIllegalConvFromXtoY) % [
|
result = convNotLegal
|
||||||
src.typeToString, castDest.typeToString])
|
elif d.Kind == tyObject and s.Kind == tyObject:
|
||||||
elif d.Kind == tyObject and s.Kind == tyObject:
|
result = checkConversionBetweenObjects(d, s)
|
||||||
checkConversionBetweenObjects(info, d, s)
|
|
||||||
elif (skipTypes(castDest, abstractVarRange).Kind in IntegralTypes) and
|
elif (skipTypes(castDest, abstractVarRange).Kind in IntegralTypes) and
|
||||||
(skipTypes(src, abstractVarRange).Kind in IntegralTypes):
|
(skipTypes(src, abstractVarRange).Kind in IntegralTypes):
|
||||||
# accept conversion between integral types
|
# accept conversion between integral types
|
||||||
else:
|
else:
|
||||||
# we use d, s here to speed up that operation a bit:
|
# we use d, s here to speed up that operation a bit:
|
||||||
case cmpTypes(d, s)
|
case cmpTypes(d, s)
|
||||||
of isNone, isGeneric:
|
of isNone, isGeneric:
|
||||||
if not compareTypes(castDest, src, dcEqIgnoreDistinct):
|
if not compareTypes(castDest, src, dcEqIgnoreDistinct):
|
||||||
LocalError(info, errGenerated, `%`(
|
result = convNotLegal
|
||||||
MsgKindToString(errIllegalConvFromXtoY),
|
else:
|
||||||
[typeToString(src), typeToString(castDest)]))
|
|
||||||
else:
|
|
||||||
nil
|
nil
|
||||||
|
|
||||||
proc isCastable(dst, src: PType): bool =
|
proc isCastable(dst, src: PType): bool =
|
||||||
|
|
@ -184,23 +188,32 @@ proc isCastable(dst, src: PType): bool =
|
||||||
(skipTypes(src, abstractInst).kind in IntegralTypes)
|
(skipTypes(src, abstractInst).kind in IntegralTypes)
|
||||||
|
|
||||||
proc isSymChoice(n: PNode): bool {.inline.} =
|
proc isSymChoice(n: PNode): bool {.inline.} =
|
||||||
result = n.kind in {nkClosedSymChoice, nkOpenSymChoice}
|
result = n.kind in nkSymChoices
|
||||||
|
|
||||||
proc semConv(c: PContext, n: PNode, s: PSym): PNode =
|
proc semConv(c: PContext, n: PNode, s: PSym): PNode =
|
||||||
if sonsLen(n) != 2:
|
if sonsLen(n) != 2:
|
||||||
LocalError(n.info, errConvNeedsOneArg)
|
LocalError(n.info, errConvNeedsOneArg)
|
||||||
return n
|
return n
|
||||||
result = newNodeI(nkConv, n.info)
|
result = newNodeI(nkConv, n.info)
|
||||||
result.typ = semTypeNode(c, n.sons[0], nil)
|
result.typ = semTypeNode(c, n.sons[0], nil).skipTypes({tyGenericInst})
|
||||||
addSon(result, copyTree(n.sons[0]))
|
addSon(result, copyTree(n.sons[0]))
|
||||||
addSon(result, semExprWithType(c, n.sons[1]))
|
addSon(result, semExprWithType(c, n.sons[1]))
|
||||||
var op = result.sons[1]
|
var op = result.sons[1]
|
||||||
|
|
||||||
if not isSymChoice(op):
|
if not isSymChoice(op):
|
||||||
checkConvertible(result.info, result.typ, op.typ)
|
let status = checkConvertible(result.typ, op.typ)
|
||||||
else:
|
case status
|
||||||
|
of convOK: nil
|
||||||
|
of convNotNeedeed:
|
||||||
|
Message(n.info, hintConvFromXtoItselfNotNeeded, result.typ.typeToString)
|
||||||
|
of convNotLegal:
|
||||||
|
LocalError(n.info, errGenerated, MsgKindToString(errIllegalConvFromXtoY)%
|
||||||
|
[op.typ.typeToString, result.typ.typeToString])
|
||||||
|
else:
|
||||||
for i in countup(0, sonsLen(op) - 1):
|
for i in countup(0, sonsLen(op) - 1):
|
||||||
let it = op.sons[i]
|
let it = op.sons[i]
|
||||||
if sameType(result.typ, it.typ):
|
let status = checkConvertible(result.typ, it.typ)
|
||||||
|
if status == convOK:
|
||||||
markUsed(n, it.sym)
|
markUsed(n, it.sym)
|
||||||
markIndirect(c, it.sym)
|
markIndirect(c, it.sym)
|
||||||
return it
|
return it
|
||||||
|
|
@ -492,12 +505,16 @@ proc analyseIfAddressTaken(c: PContext, n: PNode): PNode =
|
||||||
result = newHiddenAddrTaken(c, n) # BUGFIX!
|
result = newHiddenAddrTaken(c, n) # BUGFIX!
|
||||||
|
|
||||||
proc analyseIfAddressTakenInCall(c: PContext, n: PNode) =
|
proc analyseIfAddressTakenInCall(c: PContext, n: PNode) =
|
||||||
|
checkMinSonsLen(n, 1)
|
||||||
const
|
const
|
||||||
FakeVarParams = {mNew, mNewFinalize, mInc, ast.mDec, mIncl, mExcl,
|
FakeVarParams = {mNew, mNewFinalize, mInc, ast.mDec, mIncl, mExcl,
|
||||||
mSetLengthStr, mSetLengthSeq, mAppendStrCh, mAppendStrStr, mSwap,
|
mSetLengthStr, mSetLengthSeq, mAppendStrCh, mAppendStrStr, mSwap,
|
||||||
mAppendSeqElem, mNewSeq, mReset, mShallowCopy}
|
mAppendSeqElem, mNewSeq, mReset, mShallowCopy}
|
||||||
checkMinSonsLen(n, 1)
|
|
||||||
var t = n.sons[0].typ
|
# get the real type of the callee
|
||||||
|
# it may be a proc var with a generic alias type, so we skip over them
|
||||||
|
var t = n.sons[0].typ.skipTypes({tyGenericInst})
|
||||||
|
|
||||||
if n.sons[0].kind == nkSym and n.sons[0].sym.magic in FakeVarParams:
|
if n.sons[0].kind == nkSym and n.sons[0].sym.magic in FakeVarParams:
|
||||||
# BUGFIX: check for L-Value still needs to be done for the arguments!
|
# BUGFIX: check for L-Value still needs to be done for the arguments!
|
||||||
for i in countup(1, sonsLen(n) - 1):
|
for i in countup(1, sonsLen(n) - 1):
|
||||||
|
|
@ -618,7 +635,8 @@ proc semIndirectOp(c: PContext, n: PNode, flags: TExprFlags): PNode =
|
||||||
semOpAux(c, n)
|
semOpAux(c, n)
|
||||||
var t: PType = nil
|
var t: PType = nil
|
||||||
if (n.sons[0].typ != nil): t = skipTypes(n.sons[0].typ, abstractInst)
|
if (n.sons[0].typ != nil): t = skipTypes(n.sons[0].typ, abstractInst)
|
||||||
if (t != nil) and (t.kind == tyProc):
|
if (t != nil) and (t.kind == tyProc):
|
||||||
|
# This is a proc variable, apply normal overload resolution
|
||||||
var m: TCandidate
|
var m: TCandidate
|
||||||
initCandidate(m, t)
|
initCandidate(m, t)
|
||||||
matches(c, n, nOrig, m)
|
matches(c, n, nOrig, m)
|
||||||
|
|
@ -648,6 +666,10 @@ proc semIndirectOp(c: PContext, n: PNode, flags: TExprFlags): PNode =
|
||||||
# we assume that a procedure that calls something indirectly
|
# we assume that a procedure that calls something indirectly
|
||||||
# has side-effects:
|
# has side-effects:
|
||||||
if tfNoSideEffect notin t.flags: incl(c.p.owner.flags, sfSideEffect)
|
if tfNoSideEffect notin t.flags: incl(c.p.owner.flags, sfSideEffect)
|
||||||
|
elif (t != nil) and t.kind == tyTypeDesc:
|
||||||
|
let destType = t.skipTypes({tyTypeDesc, tyGenericInst})
|
||||||
|
result = semConv(c, n, symFromType(destType, n.info))
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
result = overloadedCallOpr(c, n)
|
result = overloadedCallOpr(c, n)
|
||||||
# Now that nkSym does not imply an iteration over the proc/iterator space,
|
# Now that nkSym does not imply an iteration over the proc/iterator space,
|
||||||
|
|
@ -956,10 +978,10 @@ proc semSubscript(c: PContext, n: PNode, flags: TExprFlags): PNode =
|
||||||
result.typ = elemType(arr)
|
result.typ = elemType(arr)
|
||||||
#GlobalError(n.info, errIndexTypesDoNotMatch)
|
#GlobalError(n.info, errIndexTypesDoNotMatch)
|
||||||
of tyTypeDesc:
|
of tyTypeDesc:
|
||||||
result = n.sons[0] # The result so far is a tyTypeDesc bound to
|
# The result so far is a tyTypeDesc bound
|
||||||
# a tyGenericBody. The line below will substitute
|
# a tyGenericBody. The line below will substitute
|
||||||
# it with the instantiated type.
|
# it with the instantiated type.
|
||||||
result.typ.sons[0] = semTypeNode(c, n, nil).linkTo(result.sym)
|
result = symNodeFromType(c, semTypeNode(c, n, nil), n.info)
|
||||||
of tyTuple:
|
of tyTuple:
|
||||||
checkSonsLen(n, 2)
|
checkSonsLen(n, 2)
|
||||||
n.sons[0] = makeDeref(n.sons[0])
|
n.sons[0] = makeDeref(n.sons[0])
|
||||||
|
|
@ -1715,12 +1737,10 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
|
||||||
of nkTableConstr:
|
of nkTableConstr:
|
||||||
result = semTableConstr(c, n)
|
result = semTableConstr(c, n)
|
||||||
of nkClosedSymChoice, nkOpenSymChoice:
|
of nkClosedSymChoice, nkOpenSymChoice:
|
||||||
LocalError(n.info, errExprXAmbiguous, renderTree(n, {renderNoComments}))
|
# handling of sym choices is context dependent
|
||||||
# error correction: Pick first element:
|
# the node is left intact for now
|
||||||
result = n.sons[0]
|
|
||||||
of nkStaticExpr:
|
of nkStaticExpr:
|
||||||
result = semStaticExpr(c, n)
|
result = semStaticExpr(c, n)
|
||||||
|
|
||||||
of nkAsgn: result = semAsgn(c, n)
|
of nkAsgn: result = semAsgn(c, n)
|
||||||
of nkBlockStmt: result = semBlock(c, n)
|
of nkBlockStmt: result = semBlock(c, n)
|
||||||
of nkStmtList: result = semStmtList(c, n)
|
of nkStmtList: result = semStmtList(c, n)
|
||||||
|
|
|
||||||
|
|
@ -685,8 +685,8 @@ proc ParamTypesMatchAux(c: PContext, m: var TCandidate, f, a: PType,
|
||||||
result = userConvMatch(c, m, base(f), a, arg)
|
result = userConvMatch(c, m, base(f), a, arg)
|
||||||
|
|
||||||
proc ParamTypesMatch(c: PContext, m: var TCandidate, f, a: PType,
|
proc ParamTypesMatch(c: PContext, m: var TCandidate, f, a: PType,
|
||||||
arg, argOrig: PNode): PNode =
|
arg, argOrig: PNode): PNode =
|
||||||
if arg == nil or arg.kind != nkClosedSymChoice:
|
if arg == nil or arg.kind notin nkSymChoices:
|
||||||
result = ParamTypesMatchAux(c, m, f, a, arg, argOrig)
|
result = ParamTypesMatchAux(c, m, f, a, arg, argOrig)
|
||||||
else:
|
else:
|
||||||
# CAUTION: The order depends on the used hashing scheme. Thus it is
|
# CAUTION: The order depends on the used hashing scheme. Thus it is
|
||||||
|
|
|
||||||
74
tests/run/tvtable.nim
Executable file
74
tests/run/tvtable.nim
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
discard """
|
||||||
|
output: '''
|
||||||
|
OBJ 1 foo
|
||||||
|
10
|
||||||
|
OBJ 1 bar
|
||||||
|
OBJ 2 foo
|
||||||
|
5
|
||||||
|
OBJ 2 bar
|
||||||
|
'''
|
||||||
|
"""
|
||||||
|
|
||||||
|
type
|
||||||
|
# these are the signatures of the virtual procs for each type
|
||||||
|
fooProc[T] = proc (o: var T): int
|
||||||
|
barProc[T] = proc (o: var T)
|
||||||
|
|
||||||
|
# an untyped table to store the proc pointers
|
||||||
|
# it's also possible to use a strontly typed tuple here
|
||||||
|
VTable = array[0..1, pointer]
|
||||||
|
|
||||||
|
TBase = object {.inheritable.}
|
||||||
|
vtbl: ptr VTable
|
||||||
|
|
||||||
|
TUserObject1 = object of TBase
|
||||||
|
x: int
|
||||||
|
|
||||||
|
TUserObject2 = object of TBase
|
||||||
|
y: int
|
||||||
|
|
||||||
|
proc foo(o: var TUserObject1): int =
|
||||||
|
echo "OBJ 1 foo"
|
||||||
|
return 10
|
||||||
|
|
||||||
|
proc bar(o: var TUserObject1) =
|
||||||
|
echo "OBJ 1 bar"
|
||||||
|
|
||||||
|
proc foo(o: var TUserObject2): int =
|
||||||
|
echo "OBJ 2 foo"
|
||||||
|
return 5
|
||||||
|
|
||||||
|
proc bar(o: var TUserObject2) =
|
||||||
|
echo "OBJ 2 bar"
|
||||||
|
|
||||||
|
proc getVTable(T: typedesc): ptr VTable =
|
||||||
|
# pay attention to what's going on here
|
||||||
|
# this will initialize the vtable for each type at program start-up
|
||||||
|
#
|
||||||
|
# fooProc[T](foo) is a type coercion - it looks for a proc named foo
|
||||||
|
# matching the signature fooProc[T] (e.g. proc (o: var TUserObject1): int)
|
||||||
|
var vtbl {.global.} = [
|
||||||
|
cast[pointer](fooProc[T](foo)),
|
||||||
|
cast[pointer](barProc[T](bar))
|
||||||
|
]
|
||||||
|
|
||||||
|
return vtbl.addr
|
||||||
|
|
||||||
|
proc create(T: typedesc): T =
|
||||||
|
result.vtbl = getVTable(T)
|
||||||
|
|
||||||
|
proc baseFoo(o: var TBase): int =
|
||||||
|
return cast[fooProc[TBase]](o.vtbl[0]) (o)
|
||||||
|
|
||||||
|
proc baseBar(o: var TBase) =
|
||||||
|
cast[barProc[TBase]](o.vtbl[1]) (o)
|
||||||
|
|
||||||
|
var a = TUserObject1.create
|
||||||
|
var b = TUserObject2.create
|
||||||
|
|
||||||
|
echo a.baseFoo
|
||||||
|
a.baseBar
|
||||||
|
|
||||||
|
echo b.baseFoo
|
||||||
|
b.baseBar
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue