Merge branch 'araq' of github.com:nim-lang/Nim into araq
This commit is contained in:
commit
053b0ad6f3
13 changed files with 49 additions and 165 deletions
|
|
@ -747,6 +747,8 @@ type
|
||||||
OnUnknown, # location is unknown (stack, heap or static)
|
OnUnknown, # location is unknown (stack, heap or static)
|
||||||
OnStatic, # in a static section
|
OnStatic, # in a static section
|
||||||
OnStack, # location is on hardware stack
|
OnStack, # location is on hardware stack
|
||||||
|
OnStackShadowDup, # location is on the stack but also replicated
|
||||||
|
# on the shadow stack
|
||||||
OnHeap # location is on heap or global
|
OnHeap # location is on heap or global
|
||||||
# (reference counting needed)
|
# (reference counting needed)
|
||||||
TLocFlags* = set[TLocFlag]
|
TLocFlags* = set[TLocFlag]
|
||||||
|
|
@ -756,6 +758,7 @@ type
|
||||||
flags*: TLocFlags # location's flags
|
flags*: TLocFlags # location's flags
|
||||||
t*: PType # type of location
|
t*: PType # type of location
|
||||||
r*: Rope # rope value of location (code generators)
|
r*: Rope # rope value of location (code generators)
|
||||||
|
dup*: Rope # duplicated location for precise stack scans
|
||||||
|
|
||||||
# ---------------- end of backend information ------------------------------
|
# ---------------- end of backend information ------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,12 +140,13 @@ var
|
||||||
gEvalExpr* = "" # expression for idetools --eval
|
gEvalExpr* = "" # expression for idetools --eval
|
||||||
gLastCmdTime*: float # when caas is enabled, we measure each command
|
gLastCmdTime*: float # when caas is enabled, we measure each command
|
||||||
gListFullPaths*: bool
|
gListFullPaths*: bool
|
||||||
isServing*: bool = false
|
gPreciseStack*: bool = false
|
||||||
gNoNimblePath* = false
|
gNoNimblePath* = false
|
||||||
gExperimentalMode*: bool
|
gExperimentalMode*: bool
|
||||||
|
|
||||||
proc importantComments*(): bool {.inline.} = gCmd in {cmdDoc, cmdIdeTools}
|
proc importantComments*(): bool {.inline.} = gCmd in {cmdDoc, cmdIdeTools}
|
||||||
proc usesNativeGC*(): bool {.inline.} = gSelectedGC >= gcRefc
|
proc usesNativeGC*(): bool {.inline.} = gSelectedGC >= gcRefc
|
||||||
|
template preciseStack*(): bool = gPreciseStack
|
||||||
|
|
||||||
template compilationCachePresent*: untyped =
|
template compilationCachePresent*: untyped =
|
||||||
{optCaasEnabled, optSymbolFiles} * gGlobalOptions != {}
|
{optCaasEnabled, optSymbolFiles} * gGlobalOptions != {}
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,6 @@ proc evalAtCompileTime(c: PContext, n: PNode): PNode =
|
||||||
result = semfold.getConstExpr(c.module, call)
|
result = semfold.getConstExpr(c.module, call)
|
||||||
if result.isNil: result = n
|
if result.isNil: result = n
|
||||||
else: return result
|
else: return result
|
||||||
result.typ = semfold.getIntervalType(callee.magic, call)
|
|
||||||
|
|
||||||
block maybeLabelAsStatic:
|
block maybeLabelAsStatic:
|
||||||
# XXX: temporary work-around needed for tlateboundstatic.
|
# XXX: temporary work-around needed for tlateboundstatic.
|
||||||
|
|
|
||||||
|
|
@ -92,26 +92,6 @@ proc pickIntRange(a, b: PType): PType =
|
||||||
proc isIntRangeOrLit(t: PType): bool =
|
proc isIntRangeOrLit(t: PType): bool =
|
||||||
result = isIntRange(t) or isIntLit(t)
|
result = isIntRange(t) or isIntLit(t)
|
||||||
|
|
||||||
proc pickMinInt(n: PNode): BiggestInt =
|
|
||||||
if n.kind in {nkIntLit..nkUInt64Lit}:
|
|
||||||
result = n.intVal
|
|
||||||
elif isIntLit(n.typ):
|
|
||||||
result = n.typ.n.intVal
|
|
||||||
elif isIntRange(n.typ):
|
|
||||||
result = firstOrd(n.typ)
|
|
||||||
else:
|
|
||||||
internalError(n.info, "pickMinInt")
|
|
||||||
|
|
||||||
proc pickMaxInt(n: PNode): BiggestInt =
|
|
||||||
if n.kind in {nkIntLit..nkUInt64Lit}:
|
|
||||||
result = n.intVal
|
|
||||||
elif isIntLit(n.typ):
|
|
||||||
result = n.typ.n.intVal
|
|
||||||
elif isIntRange(n.typ):
|
|
||||||
result = lastOrd(n.typ)
|
|
||||||
else:
|
|
||||||
internalError(n.info, "pickMaxInt")
|
|
||||||
|
|
||||||
proc makeRange(typ: PType, first, last: BiggestInt): PType =
|
proc makeRange(typ: PType, first, last: BiggestInt): PType =
|
||||||
let minA = min(first, last)
|
let minA = min(first, last)
|
||||||
let maxA = max(first, last)
|
let maxA = max(first, last)
|
||||||
|
|
@ -137,116 +117,6 @@ proc makeRangeF(typ: PType, first, last: BiggestFloat): PType =
|
||||||
result.n = n
|
result.n = n
|
||||||
addSonSkipIntLit(result, skipTypes(typ, {tyRange}))
|
addSonSkipIntLit(result, skipTypes(typ, {tyRange}))
|
||||||
|
|
||||||
proc getIntervalType*(m: TMagic, n: PNode): PType =
|
|
||||||
# Nim requires interval arithmetic for ``range`` types. Lots of tedious
|
|
||||||
# work but the feature is very nice for reducing explicit conversions.
|
|
||||||
const ordIntLit = {nkIntLit..nkUInt64Lit}
|
|
||||||
result = n.typ
|
|
||||||
|
|
||||||
template commutativeOp(opr: untyped) =
|
|
||||||
let a = n.sons[1]
|
|
||||||
let b = n.sons[2]
|
|
||||||
if isIntRangeOrLit(a.typ) and isIntRangeOrLit(b.typ):
|
|
||||||
result = makeRange(pickIntRange(a.typ, b.typ),
|
|
||||||
opr(pickMinInt(a), pickMinInt(b)),
|
|
||||||
opr(pickMaxInt(a), pickMaxInt(b)))
|
|
||||||
|
|
||||||
template binaryOp(opr: untyped) =
|
|
||||||
let a = n.sons[1]
|
|
||||||
let b = n.sons[2]
|
|
||||||
if isIntRange(a.typ) and b.kind in {nkIntLit..nkUInt64Lit}:
|
|
||||||
result = makeRange(a.typ,
|
|
||||||
opr(pickMinInt(a), pickMinInt(b)),
|
|
||||||
opr(pickMaxInt(a), pickMaxInt(b)))
|
|
||||||
|
|
||||||
case m
|
|
||||||
of mUnaryMinusI, mUnaryMinusI64:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
if isIntRange(a):
|
|
||||||
# (1..3) * (-1) == (-3.. -1)
|
|
||||||
result = makeRange(a, 0|-|lastOrd(a), 0|-|firstOrd(a))
|
|
||||||
of mUnaryMinusF64:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
if isFloatRange(a):
|
|
||||||
result = makeRangeF(a, -getFloat(a.n.sons[1]),
|
|
||||||
-getFloat(a.n.sons[0]))
|
|
||||||
of mAbsF64:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
if isFloatRange(a):
|
|
||||||
# abs(-5.. 1) == (1..5)
|
|
||||||
if a.n[0].floatVal <= 0.0:
|
|
||||||
result = makeRangeF(a, 0.0, abs(getFloat(a.n.sons[0])))
|
|
||||||
else:
|
|
||||||
result = makeRangeF(a, abs(getFloat(a.n.sons[1])),
|
|
||||||
abs(getFloat(a.n.sons[0])))
|
|
||||||
of mAbsI:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
if isIntRange(a):
|
|
||||||
if a.n[0].intVal <= 0:
|
|
||||||
result = makeRange(a, 0, `|abs|`(getInt(a.n.sons[0])))
|
|
||||||
else:
|
|
||||||
result = makeRange(a, `|abs|`(getInt(a.n.sons[1])),
|
|
||||||
`|abs|`(getInt(a.n.sons[0])))
|
|
||||||
of mSucc:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
let b = n.sons[2].typ
|
|
||||||
if isIntRange(a) and isIntLit(b):
|
|
||||||
# (-5.. 1) + 6 == (-5 + 6)..(-1 + 6)
|
|
||||||
result = makeRange(a, pickMinInt(n.sons[1]) |+| pickMinInt(n.sons[2]),
|
|
||||||
pickMaxInt(n.sons[1]) |+| pickMaxInt(n.sons[2]))
|
|
||||||
of mPred:
|
|
||||||
let a = n.sons[1].typ
|
|
||||||
let b = n.sons[2].typ
|
|
||||||
if isIntRange(a) and isIntLit(b):
|
|
||||||
result = makeRange(a, pickMinInt(n.sons[1]) |-| pickMinInt(n.sons[2]),
|
|
||||||
pickMaxInt(n.sons[1]) |-| pickMaxInt(n.sons[2]))
|
|
||||||
of mAddI, mAddU:
|
|
||||||
commutativeOp(`|+|`)
|
|
||||||
of mMulI, mMulU:
|
|
||||||
commutativeOp(`|*|`)
|
|
||||||
of mSubI, mSubU:
|
|
||||||
binaryOp(`|-|`)
|
|
||||||
of mBitandI:
|
|
||||||
# since uint64 is still not even valid for 'range' (since it's no ordinal
|
|
||||||
# yet), we exclude it from the list (see bug #1638) for now:
|
|
||||||
var a = n.sons[1]
|
|
||||||
var b = n.sons[2]
|
|
||||||
# symmetrical:
|
|
||||||
if b.kind notin ordIntLit: swap(a, b)
|
|
||||||
if b.kind in ordIntLit:
|
|
||||||
let x = b.intVal|+|1
|
|
||||||
if (x and -x) == x and x >= 0:
|
|
||||||
result = makeRange(n.typ, 0, b.intVal)
|
|
||||||
of mModU:
|
|
||||||
let a = n.sons[1]
|
|
||||||
let b = n.sons[2]
|
|
||||||
if b.kind in ordIntLit:
|
|
||||||
if b.intVal >= 0:
|
|
||||||
result = makeRange(n.typ, 0, b.intVal-1)
|
|
||||||
else:
|
|
||||||
result = makeRange(n.typ, b.intVal+1, 0)
|
|
||||||
of mModI:
|
|
||||||
# so ... if you ever wondered about modulo's signedness; this defines it:
|
|
||||||
let a = n.sons[1]
|
|
||||||
let b = n.sons[2]
|
|
||||||
if b.kind in {nkIntLit..nkUInt64Lit}:
|
|
||||||
if b.intVal >= 0:
|
|
||||||
result = makeRange(n.typ, -(b.intVal-1), b.intVal-1)
|
|
||||||
else:
|
|
||||||
result = makeRange(n.typ, b.intVal+1, -(b.intVal+1))
|
|
||||||
of mDivI, mDivU:
|
|
||||||
binaryOp(`|div|`)
|
|
||||||
of mMinI:
|
|
||||||
commutativeOp(min)
|
|
||||||
of mMaxI:
|
|
||||||
commutativeOp(max)
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
discard """
|
|
||||||
mShlI,
|
|
||||||
mShrI, mAddF64, mSubF64, mMulF64, mDivF64, mMaxF64, mMinF64
|
|
||||||
"""
|
|
||||||
|
|
||||||
proc evalIs(n, a: PNode): PNode =
|
proc evalIs(n, a: PNode): PNode =
|
||||||
# XXX: This should use the standard isOpImpl
|
# XXX: This should use the standard isOpImpl
|
||||||
internalAssert a.kind == nkSym and a.sym.kind == skType
|
internalAssert a.kind == nkSym and a.sym.kind == skType
|
||||||
|
|
|
||||||
|
|
@ -136,26 +136,6 @@ determined). Assignments from the base type to one of its subrange types
|
||||||
|
|
||||||
A subrange type has the same size as its base type (``int`` in the example).
|
A subrange type has the same size as its base type (``int`` in the example).
|
||||||
|
|
||||||
Nim requires `interval arithmetic`:idx: for subrange types over a set
|
|
||||||
of built-in operators that involve constants: ``x %% 3`` is of
|
|
||||||
type ``range[0..2]``. The following built-in operators for integers are
|
|
||||||
affected by this rule: ``-``, ``+``, ``*``, ``min``, ``max``, ``succ``,
|
|
||||||
``pred``, ``mod``, ``div``, ``%%``, ``and`` (bitwise ``and``).
|
|
||||||
|
|
||||||
Bitwise ``and`` only produces a ``range`` if one of its operands is a
|
|
||||||
constant *x* so that (x+1) is a power of two.
|
|
||||||
(Bitwise ``and`` is then a ``%%`` operation.)
|
|
||||||
|
|
||||||
This means that the following code is accepted:
|
|
||||||
|
|
||||||
.. code-block:: nim
|
|
||||||
case (x and 3) + 7
|
|
||||||
of 7: echo "A"
|
|
||||||
of 8: echo "B"
|
|
||||||
of 9: echo "C"
|
|
||||||
of 10: echo "D"
|
|
||||||
# note: no ``else`` required as (x and 3) + 7 has the type: range[7..10]
|
|
||||||
|
|
||||||
|
|
||||||
Pre-defined floating point types
|
Pre-defined floating point types
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
|
||||||
|
|
@ -669,7 +669,7 @@ proc astGenRepr*(n: NimNode): string {.compileTime, benign.} =
|
||||||
t = x
|
t = x
|
||||||
result = newString(len)
|
result = newString(len)
|
||||||
for j in countdown(len-1, 0):
|
for j in countdown(len-1, 0):
|
||||||
result[j] = HexChars[t and 0xF]
|
result[j] = HexChars[int(t and 0xF)]
|
||||||
t = t shr 4
|
t = t shr 4
|
||||||
# handle negative overflow
|
# handle negative overflow
|
||||||
if t == 0 and x < 0: t = -1
|
if t == 0 and x < 0: t = -1
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,10 @@ proc encode(dest: var MD5Block, src: cstring) =
|
||||||
proc decode(dest: var openArray[uint8], src: openArray[uint32]) =
|
proc decode(dest: var openArray[uint8], src: openArray[uint32]) =
|
||||||
var i = 0
|
var i = 0
|
||||||
for j in 0..high(src):
|
for j in 0..high(src):
|
||||||
dest[i] = src[j] and 0xff'u32
|
dest[i] = uint8(src[j] and 0xff'u32)
|
||||||
dest[i+1] = src[j] shr 8 and 0xff'u32
|
dest[i+1] = uint8(src[j] shr 8 and 0xff'u32)
|
||||||
dest[i+2] = src[j] shr 16 and 0xff'u32
|
dest[i+2] = uint8(src[j] shr 16 and 0xff'u32)
|
||||||
dest[i+3] = src[j] shr 24 and 0xff'u32
|
dest[i+3] = uint8(src[j] shr 24 and 0xff'u32)
|
||||||
inc(i, 4)
|
inc(i, 4)
|
||||||
|
|
||||||
proc transform(buffer: pointer, state: var MD5State) =
|
proc transform(buffer: pointer, state: var MD5State) =
|
||||||
|
|
@ -216,8 +216,8 @@ proc `$`*(d: MD5Digest): string =
|
||||||
const digits = "0123456789abcdef"
|
const digits = "0123456789abcdef"
|
||||||
result = ""
|
result = ""
|
||||||
for i in 0..15:
|
for i in 0..15:
|
||||||
add(result, digits[(d[i] shr 4) and 0xF])
|
add(result, digits[(d[i].int shr 4) and 0xF])
|
||||||
add(result, digits[d[i] and 0xF])
|
add(result, digits[d[i].int and 0xF])
|
||||||
|
|
||||||
proc getMD5*(s: string): string =
|
proc getMD5*(s: string): string =
|
||||||
## computes an MD5 value of `s` and returns its string representation
|
## computes an MD5 value of `s` and returns its string representation
|
||||||
|
|
|
||||||
|
|
@ -148,13 +148,13 @@ proc sha1(src: cstring; len: int): Sha1Digest =
|
||||||
while lastBlockBytes < endCurrentBlock:
|
while lastBlockBytes < endCurrentBlock:
|
||||||
|
|
||||||
var value = uint32(src[lastBlockBytes + currentBlock]) shl
|
var value = uint32(src[lastBlockBytes + currentBlock]) shl
|
||||||
((3'u32 - (lastBlockBytes and 3)) shl 3)
|
((3'u32 - uint32(lastBlockBytes and 3)) shl 3)
|
||||||
|
|
||||||
w[lastBlockBytes shr 2] = w[lastBlockBytes shr 2] or value
|
w[lastBlockBytes shr 2] = w[lastBlockBytes shr 2] or value
|
||||||
inc(lastBlockBytes)
|
inc(lastBlockBytes)
|
||||||
|
|
||||||
w[lastBlockBytes shr 2] = w[lastBlockBytes shr 2] or (
|
w[lastBlockBytes shr 2] = w[lastBlockBytes shr 2] or (
|
||||||
0x80'u32 shl ((3'u32 - (lastBlockBytes and 3)) shl 3)
|
0x80'u32 shl ((3'u32 - uint32(lastBlockBytes and 3)) shl 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
if endCurrentBlock >= 56:
|
if endCurrentBlock >= 56:
|
||||||
|
|
|
||||||
|
|
@ -887,7 +887,7 @@ proc toHex*(x: BiggestInt, len: Positive): string {.noSideEffect,
|
||||||
n = x
|
n = x
|
||||||
result = newString(len)
|
result = newString(len)
|
||||||
for j in countdown(len-1, 0):
|
for j in countdown(len-1, 0):
|
||||||
result[j] = HexChars[n and 0xF]
|
result[j] = HexChars[(n and 0xF).int]
|
||||||
n = n shr 4
|
n = n shr 4
|
||||||
# handle negative overflow
|
# handle negative overflow
|
||||||
if n == 0 and x < 0: n = -1
|
if n == 0 and x < 0: n = -1
|
||||||
|
|
|
||||||
|
|
@ -1909,7 +1909,7 @@ const
|
||||||
NimMinor*: int = 17
|
NimMinor*: int = 17
|
||||||
## is the minor number of Nim's version.
|
## is the minor number of Nim's version.
|
||||||
|
|
||||||
NimPatch*: int = 2
|
NimPatch*: int = 3
|
||||||
## is the patch number of Nim's version.
|
## is the patch number of Nim's version.
|
||||||
|
|
||||||
NimVersion*: string = $NimMajor & "." & $NimMinor & "." & $NimPatch
|
NimVersion*: string = $NimMajor & "." & $NimMinor & "." & $NimPatch
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,29 @@ proc chckRange(i, a, b: int): int {.inline, compilerproc, benign.}
|
||||||
proc chckRangeF(x, a, b: float): float {.inline, compilerproc, benign.}
|
proc chckRangeF(x, a, b: float): float {.inline, compilerproc, benign.}
|
||||||
proc chckNil(p: pointer) {.noinline, compilerproc, benign.}
|
proc chckNil(p: pointer) {.noinline, compilerproc, benign.}
|
||||||
|
|
||||||
|
type
|
||||||
|
GcFrame = ptr GcFrameHeader
|
||||||
|
GcFrameHeader {.compilerproc.} = object
|
||||||
|
len: int
|
||||||
|
prev: ptr GcFrameHeader
|
||||||
|
|
||||||
var
|
var
|
||||||
framePtr {.threadvar.}: PFrame
|
framePtr {.threadvar.}: PFrame
|
||||||
excHandler {.threadvar.}: PSafePoint
|
excHandler {.threadvar.}: PSafePoint
|
||||||
# list of exception handlers
|
# list of exception handlers
|
||||||
# a global variable for the root of all try blocks
|
# a global variable for the root of all try blocks
|
||||||
currException {.threadvar.}: ref Exception
|
currException {.threadvar.}: ref Exception
|
||||||
|
gcFramePtr {.threadvar.}: GcFrame
|
||||||
|
|
||||||
type
|
type
|
||||||
FrameState = tuple[framePtr: PFrame, excHandler: PSafePoint, currException: ref Exception]
|
FrameState = tuple[gcFramePtr: GcFrame, framePtr: PFrame,
|
||||||
|
excHandler: PSafePoint, currException: ref Exception]
|
||||||
|
|
||||||
proc getFrameState*(): FrameState {.compilerRtl, inl.} =
|
proc getFrameState*(): FrameState {.compilerRtl, inl.} =
|
||||||
return (framePtr, excHandler, currException)
|
return (gcFramePtr, framePtr, excHandler, currException)
|
||||||
|
|
||||||
proc setFrameState*(state: FrameState) {.compilerRtl, inl.} =
|
proc setFrameState*(state: FrameState) {.compilerRtl, inl.} =
|
||||||
|
gcFramePtr = state.gcFramePtr
|
||||||
framePtr = state.framePtr
|
framePtr = state.framePtr
|
||||||
excHandler = state.excHandler
|
excHandler = state.excHandler
|
||||||
currException = state.currException
|
currException = state.currException
|
||||||
|
|
@ -64,6 +73,14 @@ proc popFrame {.compilerRtl, inl.} =
|
||||||
proc setFrame*(s: PFrame) {.compilerRtl, inl.} =
|
proc setFrame*(s: PFrame) {.compilerRtl, inl.} =
|
||||||
framePtr = s
|
framePtr = s
|
||||||
|
|
||||||
|
proc getGcFrame*(): GcFrame {.compilerRtl, inl.} = gcFramePtr
|
||||||
|
proc popGcFrame*() {.compilerRtl, inl.} = gcFramePtr = gcFramePtr.prev
|
||||||
|
proc setGcFrame*(s: GcFrame) {.compilerRtl, inl.} = gcFramePtr = s
|
||||||
|
proc pushGcFrame*(s: GcFrame) {.compilerRtl, inl.} =
|
||||||
|
s.prev = gcFramePtr
|
||||||
|
zeroMem(cast[pointer](cast[int](s)+%sizeof(GcFrameHeader)), s.len*sizeof(pointer))
|
||||||
|
gcFramePtr = s
|
||||||
|
|
||||||
proc pushSafePoint(s: PSafePoint) {.compilerRtl, inl.} =
|
proc pushSafePoint(s: PSafePoint) {.compilerRtl, inl.} =
|
||||||
s.hasRaiseAction = false
|
s.hasRaiseAction = false
|
||||||
s.prev = excHandler
|
s.prev = excHandler
|
||||||
|
|
|
||||||
13
tests/arithm/tsubrange.nim
Normal file
13
tests/arithm/tsubrange.nim
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
discard """
|
||||||
|
output: '''1'''
|
||||||
|
"""
|
||||||
|
|
||||||
|
# bug #5854
|
||||||
|
type
|
||||||
|
n16* = range[0'i16..high(int16)]
|
||||||
|
|
||||||
|
var level: n16 = 1
|
||||||
|
let maxLevel: n16 = 1
|
||||||
|
|
||||||
|
level = min(level + 2, maxLevel)
|
||||||
|
echo level
|
||||||
1
todo.txt
1
todo.txt
|
|
@ -1,6 +1,7 @@
|
||||||
version 1.0 battle plan
|
version 1.0 battle plan
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
- eliminate 'nil' for seqs and strings
|
||||||
- make 'not nil' the default (produce warnings instead of errors for
|
- make 'not nil' the default (produce warnings instead of errors for
|
||||||
a smooth migration path)
|
a smooth migration path)
|
||||||
- case objects needs to be safe and need to support pattern matching
|
- case objects needs to be safe and need to support pattern matching
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue