simple unit test and better documentation for the user defined type classes
This commit is contained in:
parent
4cea15d274
commit
a068aaed3c
8 changed files with 113 additions and 57 deletions
|
|
@ -437,6 +437,13 @@ type
|
||||||
line*, col*: int16
|
line*, col*: int16
|
||||||
fileIndex*: int32
|
fileIndex*: int32
|
||||||
|
|
||||||
|
TErrorOutput* = enum
|
||||||
|
eStdOut
|
||||||
|
eStdErr
|
||||||
|
eInMemory
|
||||||
|
|
||||||
|
TErrorOutputs* = set[TErrorOutput]
|
||||||
|
|
||||||
ERecoverableError* = object of EInvalidValue
|
ERecoverableError* = object of EInvalidValue
|
||||||
ESuggestDone* = object of EBase
|
ESuggestDone* = object of EBase
|
||||||
|
|
||||||
|
|
@ -534,13 +541,27 @@ var
|
||||||
gHintCounter*: int = 0
|
gHintCounter*: int = 0
|
||||||
gWarnCounter*: int = 0
|
gWarnCounter*: int = 0
|
||||||
gErrorMax*: int = 1 # stop after gErrorMax errors
|
gErrorMax*: int = 1 # stop after gErrorMax errors
|
||||||
gSilence*: int # == 0 if we produce any output at all
|
|
||||||
|
|
||||||
when useCaas:
|
when useCaas:
|
||||||
var stdoutSocket*: TSocket
|
var stdoutSocket*: TSocket
|
||||||
|
|
||||||
|
proc UnknownLineInfo*(): TLineInfo =
|
||||||
|
result.line = int16(-1)
|
||||||
|
result.col = int16(-1)
|
||||||
|
result.fileIndex = -1
|
||||||
|
|
||||||
|
var
|
||||||
|
msgContext: seq[TLineInfo] = @[]
|
||||||
|
lastError = UnknownLineInfo()
|
||||||
|
bufferedMsgs*: seq[string]
|
||||||
|
|
||||||
|
errorOutputs* = {eStdOut, eStdErr}
|
||||||
|
|
||||||
|
proc clearBufferedMsgs* =
|
||||||
|
bufferedMsgs = nil
|
||||||
|
|
||||||
proc SuggestWriteln*(s: string) =
|
proc SuggestWriteln*(s: string) =
|
||||||
if gSilence == 0:
|
if eStdOut in errorOutputs:
|
||||||
when useCaas:
|
when useCaas:
|
||||||
if isNil(stdoutSocket): Writeln(stdout, s)
|
if isNil(stdoutSocket): Writeln(stdout, s)
|
||||||
else:
|
else:
|
||||||
|
|
@ -549,6 +570,9 @@ proc SuggestWriteln*(s: string) =
|
||||||
else:
|
else:
|
||||||
Writeln(stdout, s)
|
Writeln(stdout, s)
|
||||||
|
|
||||||
|
if eInMemory in errorOutputs:
|
||||||
|
bufferedMsgs.safeAdd(s)
|
||||||
|
|
||||||
proc SuggestQuit*() =
|
proc SuggestQuit*() =
|
||||||
if not isServing:
|
if not isServing:
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
@ -570,14 +594,6 @@ const
|
||||||
RawWarningFormat* = "Warning: $1"
|
RawWarningFormat* = "Warning: $1"
|
||||||
RawHintFormat* = "Hint: $1"
|
RawHintFormat* = "Hint: $1"
|
||||||
|
|
||||||
proc UnknownLineInfo*(): TLineInfo =
|
|
||||||
result.line = int16(-1)
|
|
||||||
result.col = int16(-1)
|
|
||||||
result.fileIndex = -1
|
|
||||||
|
|
||||||
var
|
|
||||||
msgContext: seq[TLineInfo] = @[]
|
|
||||||
|
|
||||||
proc getInfoContextLen*(): int = return msgContext.len
|
proc getInfoContextLen*(): int = return msgContext.len
|
||||||
proc setInfoContextLen*(L: int) = setLen(msgContext, L)
|
proc setInfoContextLen*(L: int) = setLen(msgContext, L)
|
||||||
|
|
||||||
|
|
@ -642,14 +658,18 @@ proc addCheckpoint*(filename: string, line: int) =
|
||||||
|
|
||||||
proc OutWriteln*(s: string) =
|
proc OutWriteln*(s: string) =
|
||||||
## Writes to stdout. Always.
|
## Writes to stdout. Always.
|
||||||
if gSilence == 0: Writeln(stdout, s)
|
if eStdOut in errorOutputs: Writeln(stdout, s)
|
||||||
|
|
||||||
proc MsgWriteln*(s: string) =
|
proc MsgWriteln*(s: string) =
|
||||||
## Writes to stdout. If --stdout option is given, writes to stderr instead.
|
## Writes to stdout. If --stdout option is given, writes to stderr instead.
|
||||||
if gSilence == 0:
|
|
||||||
if gCmd == cmdIdeTools and optCDebug notin gGlobalOptions: return
|
if gCmd == cmdIdeTools and optCDebug notin gGlobalOptions: return
|
||||||
if optStdout in gGlobalOptions: Writeln(stderr, s)
|
|
||||||
else: Writeln(stdout, s)
|
if optStdout in gGlobalOptions:
|
||||||
|
if eStdErr in errorOutputs: Writeln(stderr, s)
|
||||||
|
else:
|
||||||
|
if eStdOut in errorOutputs: Writeln(stdout, s)
|
||||||
|
|
||||||
|
if eInMemory in errorOutputs: bufferedMsgs.safeAdd(s)
|
||||||
|
|
||||||
proc coordToStr(coord: int): string =
|
proc coordToStr(coord: int): string =
|
||||||
if coord == -1: result = "???"
|
if coord == -1: result = "???"
|
||||||
|
|
@ -736,9 +756,6 @@ proc rawMessage*(msg: TMsgKind, args: openarray[string]) =
|
||||||
proc rawMessage*(msg: TMsgKind, arg: string) =
|
proc rawMessage*(msg: TMsgKind, arg: string) =
|
||||||
rawMessage(msg, [arg])
|
rawMessage(msg, [arg])
|
||||||
|
|
||||||
var
|
|
||||||
lastError = UnknownLineInfo()
|
|
||||||
|
|
||||||
proc writeSurroundingSrc(info: TLineInfo) =
|
proc writeSurroundingSrc(info: TLineInfo) =
|
||||||
const indent = " "
|
const indent = " "
|
||||||
MsgWriteln(indent & info.sourceLine.ropeToStr)
|
MsgWriteln(indent & info.sourceLine.ropeToStr)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ proc semParamList(c: PContext, n, genericParams: PNode, s: PSym)
|
||||||
proc addParams(c: PContext, n: PNode, kind: TSymKind)
|
proc addParams(c: PContext, n: PNode, kind: TSymKind)
|
||||||
proc maybeAddResult(c: PContext, s: PSym, n: PNode)
|
proc maybeAddResult(c: PContext, s: PSym, n: PNode)
|
||||||
proc instGenericContainer(c: PContext, n: PNode, header: PType): PType
|
proc instGenericContainer(c: PContext, n: PNode, header: PType): PType
|
||||||
proc tryExpr(c: PContext, n: PNode, flags: TExprFlags = {}): PNode
|
proc tryExpr(c: PContext, n: PNode,
|
||||||
|
flags: TExprFlags = {}, bufferErrors = false): PNode
|
||||||
proc fixImmediateParams(n: PNode): PNode
|
proc fixImmediateParams(n: PNode): PNode
|
||||||
proc activate(c: PContext, n: PNode)
|
proc activate(c: PContext, n: PNode)
|
||||||
proc semQuoteAst(c: PContext, n: PNode): PNode
|
proc semQuoteAst(c: PContext, n: PNode): PNode
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ type
|
||||||
libs*: TLinkedList # all libs used by this module
|
libs*: TLinkedList # all libs used by this module
|
||||||
semConstExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.} # for the pragmas
|
semConstExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.} # for the pragmas
|
||||||
semExpr*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
|
semExpr*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
|
||||||
semTryExpr*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
|
semTryExpr*: proc (c: PContext, n: PNode,flags: TExprFlags = {},
|
||||||
|
bufferErrors = false): PNode {.nimcall.}
|
||||||
semOperand*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
|
semOperand*: proc (c: PContext, n: PNode, flags: TExprFlags = {}): PNode {.nimcall.}
|
||||||
semConstBoolExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.} # XXX bite the bullet
|
semConstBoolExpr*: proc (c: PContext, n: PNode): PNode {.nimcall.} # XXX bite the bullet
|
||||||
semOverloadedCall*: proc (c: PContext, n, nOrig: PNode,
|
semOverloadedCall*: proc (c: PContext, n, nOrig: PNode,
|
||||||
|
|
|
||||||
|
|
@ -824,7 +824,7 @@ proc buildEchoStmt(c: PContext, n: PNode): PNode =
|
||||||
|
|
||||||
proc semExprNoType(c: PContext, n: PNode): PNode =
|
proc semExprNoType(c: PContext, n: PNode): PNode =
|
||||||
result = semExpr(c, n, {efWantStmt})
|
result = semExpr(c, n, {efWantStmt})
|
||||||
discardCheck(result)
|
discardCheck(c, result)
|
||||||
|
|
||||||
proc isTypeExpr(n: PNode): bool =
|
proc isTypeExpr(n: PNode): bool =
|
||||||
case n.kind
|
case n.kind
|
||||||
|
|
@ -1218,7 +1218,7 @@ proc semProcBody(c: PContext, n: PNode): PNode =
|
||||||
a.sons[1] = result
|
a.sons[1] = result
|
||||||
result = semAsgn(c, a)
|
result = semAsgn(c, a)
|
||||||
else:
|
else:
|
||||||
discardCheck(result)
|
discardCheck(c, result)
|
||||||
closeScope(c)
|
closeScope(c)
|
||||||
|
|
||||||
proc SemYieldVarResult(c: PContext, n: PNode, restype: PType) =
|
proc SemYieldVarResult(c: PContext, n: PNode, restype: PType) =
|
||||||
|
|
@ -1439,12 +1439,12 @@ proc semQuoteAst(c: PContext, n: PNode): PNode =
|
||||||
newNode(nkCall, n.info, quotes)])
|
newNode(nkCall, n.info, quotes)])
|
||||||
result = semExpandToAst(c, result)
|
result = semExpandToAst(c, result)
|
||||||
|
|
||||||
proc tryExpr(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
|
proc tryExpr(c: PContext, n: PNode,
|
||||||
|
flags: TExprFlags = {}, bufferErrors = false): PNode =
|
||||||
# watch out, hacks ahead:
|
# watch out, hacks ahead:
|
||||||
let oldErrorCount = msgs.gErrorCounter
|
let oldErrorCount = msgs.gErrorCounter
|
||||||
let oldErrorMax = msgs.gErrorMax
|
let oldErrorMax = msgs.gErrorMax
|
||||||
inc c.InCompilesContext
|
inc c.InCompilesContext
|
||||||
inc msgs.gSilence
|
|
||||||
# do not halt after first error:
|
# do not halt after first error:
|
||||||
msgs.gErrorMax = high(int)
|
msgs.gErrorMax = high(int)
|
||||||
|
|
||||||
|
|
@ -1453,6 +1453,8 @@ proc tryExpr(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
|
||||||
openScope(c)
|
openScope(c)
|
||||||
let oldOwnerLen = len(gOwners)
|
let oldOwnerLen = len(gOwners)
|
||||||
let oldGenerics = c.generics
|
let oldGenerics = c.generics
|
||||||
|
let oldErrorOutputs = errorOutputs
|
||||||
|
errorOutputs = if bufferErrors: {eInMemory} else: {}
|
||||||
let oldContextLen = msgs.getInfoContextLen()
|
let oldContextLen = msgs.getInfoContextLen()
|
||||||
|
|
||||||
let oldInGenericContext = c.InGenericContext
|
let oldInGenericContext = c.InGenericContext
|
||||||
|
|
@ -1475,7 +1477,7 @@ proc tryExpr(c: PContext, n: PNode, flags: TExprFlags = {}): PNode =
|
||||||
setlen(gOwners, oldOwnerLen)
|
setlen(gOwners, oldOwnerLen)
|
||||||
c.currentScope = oldScope
|
c.currentScope = oldScope
|
||||||
dec c.InCompilesContext
|
dec c.InCompilesContext
|
||||||
dec msgs.gSilence
|
errorOutputs = oldErrorOutputs
|
||||||
msgs.gErrorCounter = oldErrorCount
|
msgs.gErrorCounter = oldErrorCount
|
||||||
msgs.gErrorMax = oldErrorMax
|
msgs.gErrorMax = oldErrorMax
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ proc fixNilType(n: PNode) =
|
||||||
for it in n: fixNilType(it)
|
for it in n: fixNilType(it)
|
||||||
n.typ = nil
|
n.typ = nil
|
||||||
|
|
||||||
proc discardCheck(result: PNode) =
|
proc discardCheck(c: PContext, result: PNode) =
|
||||||
if result.typ != nil and result.typ.kind notin {tyStmt, tyEmpty}:
|
if result.typ != nil and result.typ.kind notin {tyStmt, tyEmpty}:
|
||||||
if result.kind == nkNilLit:
|
if result.kind == nkNilLit:
|
||||||
result.typ = nil
|
result.typ = nil
|
||||||
|
|
@ -142,6 +142,10 @@ proc discardCheck(result: PNode) =
|
||||||
while n.kind in skipForDiscardable:
|
while n.kind in skipForDiscardable:
|
||||||
n = n.lastSon
|
n = n.lastSon
|
||||||
n.typ = nil
|
n.typ = nil
|
||||||
|
elif c.InTypeClass > 0 and result.typ.kind == tyBool:
|
||||||
|
let verdict = semConstExpr(c, result)
|
||||||
|
if verdict.intVal == 0:
|
||||||
|
localError(result.info, "type class predicate failed.")
|
||||||
elif result.typ.kind != tyError and gCmd != cmdInteractive:
|
elif result.typ.kind != tyError and gCmd != cmdInteractive:
|
||||||
if result.typ.kind == tyNil:
|
if result.typ.kind == tyNil:
|
||||||
fixNilType(result)
|
fixNilType(result)
|
||||||
|
|
@ -169,7 +173,7 @@ proc semIf(c: PContext, n: PNode): PNode =
|
||||||
typ = commonType(typ, it.sons[0].typ)
|
typ = commonType(typ, it.sons[0].typ)
|
||||||
else: illFormedAst(it)
|
else: illFormedAst(it)
|
||||||
if isEmptyType(typ) or typ.kind == tyNil or not hasElse:
|
if isEmptyType(typ) or typ.kind == tyNil or not hasElse:
|
||||||
for it in n: discardCheck(it.lastSon)
|
for it in n: discardCheck(c, it.lastSon)
|
||||||
result.kind = nkIfStmt
|
result.kind = nkIfStmt
|
||||||
# propagate any enforced VoidContext:
|
# propagate any enforced VoidContext:
|
||||||
if typ == EnforceVoidContext: result.typ = EnforceVoidContext
|
if typ == EnforceVoidContext: result.typ = EnforceVoidContext
|
||||||
|
|
@ -230,7 +234,7 @@ proc semCase(c: PContext, n: PNode): PNode =
|
||||||
localError(n.info, errNotAllCasesCovered)
|
localError(n.info, errNotAllCasesCovered)
|
||||||
closeScope(c)
|
closeScope(c)
|
||||||
if isEmptyType(typ) or typ.kind == tyNil or not hasElse:
|
if isEmptyType(typ) or typ.kind == tyNil or not hasElse:
|
||||||
for i in 1..n.len-1: discardCheck(n.sons[i].lastSon)
|
for i in 1..n.len-1: discardCheck(c, n.sons[i].lastSon)
|
||||||
# propagate any enforced VoidContext:
|
# propagate any enforced VoidContext:
|
||||||
if typ == EnforceVoidContext:
|
if typ == EnforceVoidContext:
|
||||||
result.typ = EnforceVoidContext
|
result.typ = EnforceVoidContext
|
||||||
|
|
@ -275,8 +279,8 @@ proc semTry(c: PContext, n: PNode): PNode =
|
||||||
typ = commonType(typ, a.sons[length-1].typ)
|
typ = commonType(typ, a.sons[length-1].typ)
|
||||||
dec c.p.inTryStmt
|
dec c.p.inTryStmt
|
||||||
if isEmptyType(typ) or typ.kind == tyNil:
|
if isEmptyType(typ) or typ.kind == tyNil:
|
||||||
discardCheck(n.sons[0])
|
discardCheck(c, n.sons[0])
|
||||||
for i in 1..n.len-1: discardCheck(n.sons[i].lastSon)
|
for i in 1..n.len-1: discardCheck(c, n.sons[i].lastSon)
|
||||||
if typ == EnforceVoidContext:
|
if typ == EnforceVoidContext:
|
||||||
result.typ = EnforceVoidContext
|
result.typ = EnforceVoidContext
|
||||||
else:
|
else:
|
||||||
|
|
@ -1221,7 +1225,7 @@ proc semStmtList(c: PContext, n: PNode): PNode =
|
||||||
voidContext = true
|
voidContext = true
|
||||||
n.typ = EnforceVoidContext
|
n.typ = EnforceVoidContext
|
||||||
if i != last or voidContext:
|
if i != last or voidContext:
|
||||||
discardCheck(n.sons[i])
|
discardCheck(c, n.sons[i])
|
||||||
else:
|
else:
|
||||||
n.typ = n.sons[i].typ
|
n.typ = n.sons[i].typ
|
||||||
if not isEmptyType(n.typ):
|
if not isEmptyType(n.typ):
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ proc initCandidate*(c: var TCandidate, callee: PSym, binding: PNode,
|
||||||
c.calleeSym = callee
|
c.calleeSym = callee
|
||||||
c.calleeScope = calleeScope
|
c.calleeScope = calleeScope
|
||||||
initIdTable(c.bindings)
|
initIdTable(c.bindings)
|
||||||
|
c.errors = nil
|
||||||
if binding != nil and callee.kind in RoutineKinds:
|
if binding != nil and callee.kind in RoutineKinds:
|
||||||
var typeParams = callee.ast[genericParamsPos]
|
var typeParams = callee.ast[genericParamsPos]
|
||||||
for i in 1..min(sonsLen(typeParams), sonsLen(binding)-1):
|
for i in 1..min(sonsLen(typeParams), sonsLen(binding)-1):
|
||||||
|
|
@ -774,23 +775,16 @@ proc matchUserTypeClass*(c: PContext, m: var TCandidate,
|
||||||
addDecl(c, dummyParam)
|
addDecl(c, dummyParam)
|
||||||
|
|
||||||
for stmt in f.n[3]:
|
for stmt in f.n[3]:
|
||||||
var e = c.semTryExpr(c, copyTree(stmt))
|
var e = c.semTryExpr(c, copyTree(stmt), bufferErrors = false)
|
||||||
if e == nil:
|
m.errors = bufferedMsgs
|
||||||
let expStr = renderTree(stmt, {renderNoComments})
|
clearBufferedMsgs()
|
||||||
m.errors.safeAdd("can't compile " & expStr & " for " & a.typeToString)
|
if e == nil: return nil
|
||||||
return nil
|
|
||||||
case e.kind
|
case e.kind
|
||||||
of nkReturnStmt:
|
of nkReturnStmt: nil
|
||||||
nil
|
|
||||||
of nkTypeSection: nil
|
of nkTypeSection: nil
|
||||||
of nkConstDef: nil
|
of nkConstDef: nil
|
||||||
else:
|
else: nil
|
||||||
if e.typ != nil and e.typ.kind == tyBool:
|
|
||||||
let verdict = c.semConstExpr(c, e)
|
|
||||||
if verdict.intVal == 0:
|
|
||||||
let expStr = renderTree(stmt, {renderNoComments})
|
|
||||||
m.errors.safeAdd(expStr & " doesn't hold for " & a.typeToString)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
result = arg
|
result = arg
|
||||||
put(m.bindings, f, a)
|
put(m.bindings, f, a)
|
||||||
|
|
|
||||||
|
|
@ -3289,27 +3289,36 @@ Declarative type classes are written in the following form:
|
||||||
for value in c:
|
for value in c:
|
||||||
type(value) is T
|
type(value) is T
|
||||||
|
|
||||||
|
The type class will be matched if:
|
||||||
The identifiers following the `generic` keyword are treated as variables of
|
|
||||||
the matched type and the body of the type class consists of arbitrary code that
|
|
||||||
must be valid under these circumstances.
|
|
||||||
|
|
||||||
Specifically, the type class will be matched if:
|
|
||||||
|
|
||||||
a) all of the expressions within the body can be compiled for the tested type
|
a) all of the expressions within the body can be compiled for the tested type
|
||||||
b) all statically evaluatable boolean expressions in the body must be true
|
b) all statically evaluatable boolean expressions in the body must be true
|
||||||
|
|
||||||
Please note that the ``is`` operator allows you to easily verify the precise type
|
The identifiers following the `generic` keyword represent instances of the
|
||||||
signatures of the required operations, but since type inference and default
|
currently matched type. These instances can act both as variables of the type,
|
||||||
parameters are still applied in the provided block, it's also possible to encode
|
when used in contexts, where a value is expected, and as the type itself, when
|
||||||
usage protocols that doesn't reveal implementation details.
|
used in a contexts, where a type is expected.
|
||||||
|
|
||||||
|
Please note that the ``is`` operator allows you to easily verify the precise
|
||||||
|
type signatures of the required operations, but since type inference and
|
||||||
|
default parameters are still applied in the provided block, it's also possible
|
||||||
|
to encode usage protocols that doesn't reveal implementation details.
|
||||||
|
|
||||||
|
As a special rule providing further convenience when writing type classes, any
|
||||||
|
type value appearing in a callable expression will be treated as a variable of
|
||||||
|
the designated type for overload resolution purposes, unless the type value was
|
||||||
|
passed in its explicit ``typedesc[T]`` form:
|
||||||
|
|
||||||
|
.. code-block:: nimrod
|
||||||
|
type
|
||||||
|
OutputStream = generic S
|
||||||
|
write(var S, string)
|
||||||
|
|
||||||
Much like generics, the user defined type classes will be instantiated exactly
|
Much like generics, the user defined type classes will be instantiated exactly
|
||||||
once for each tested type and any static code included within them will also be
|
once for each tested type and any static code included within them will also be
|
||||||
executed once.
|
executed once.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Return Type Inference
|
Return Type Inference
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
||||||
28
tests/run/tusertypeclasses.nim
Normal file
28
tests/run/tusertypeclasses.nim
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
discard """
|
||||||
|
output: "Sortable\nSortable\nContainer"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typetraits
|
||||||
|
|
||||||
|
type
|
||||||
|
TObj = object
|
||||||
|
x: int
|
||||||
|
|
||||||
|
Sortable = generic x, y
|
||||||
|
(x < y) is bool
|
||||||
|
|
||||||
|
ObjectContainer = generic C
|
||||||
|
C.len is ordinal
|
||||||
|
for v in items(C):
|
||||||
|
v.type is tuple|object
|
||||||
|
|
||||||
|
proc foo(c: ObjectContainer) =
|
||||||
|
echo "Container"
|
||||||
|
|
||||||
|
proc foo(x: Sortable) =
|
||||||
|
echo "Sortable"
|
||||||
|
|
||||||
|
foo 10
|
||||||
|
foo "test"
|
||||||
|
foo(@[TObj(x: 10), TObj(x: 20)])
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue