added a 'koch ic' command for easier adhoc testing of IC (#17508)
* added a 'koch ic' command for easier adhoc testing of IC * IC: progress * IC: enable generics test * make tests green
This commit is contained in:
parent
8049d0befc
commit
e3e9742143
8 changed files with 96 additions and 14 deletions
|
|
@ -1357,9 +1357,6 @@ proc genTypeInfoV2Impl(m: BModule, t, origType: PType, name: Rope; info: TLineIn
|
||||||
if t.kind == tyObject and t.len > 0 and t[0] != nil and optEnableDeepCopy in m.config.globalOptions:
|
if t.kind == tyObject and t.len > 0 and t[0] != nil and optEnableDeepCopy in m.config.globalOptions:
|
||||||
discard genTypeInfoV1(m, t, info)
|
discard genTypeInfoV1(m, t, info)
|
||||||
|
|
||||||
proc moduleOpenForCodegen(m: BModule; module: int32): bool {.inline.} =
|
|
||||||
result = module < m.g.modules.len and m.g.modules[module] != nil
|
|
||||||
|
|
||||||
proc genTypeInfoV2(m: BModule, t: PType; info: TLineInfo): Rope =
|
proc genTypeInfoV2(m: BModule, t: PType; info: TLineInfo): Rope =
|
||||||
let origType = t
|
let origType = t
|
||||||
# distinct types can have their own destructors
|
# distinct types can have their own destructors
|
||||||
|
|
@ -1384,7 +1381,7 @@ proc genTypeInfoV2(m: BModule, t: PType; info: TLineInfo): Rope =
|
||||||
m.typeInfoMarkerV2[sig] = result
|
m.typeInfoMarkerV2[sig] = result
|
||||||
|
|
||||||
let owner = t.skipTypes(typedescPtrs).itemId.module
|
let owner = t.skipTypes(typedescPtrs).itemId.module
|
||||||
if owner != m.module.position and moduleOpenForCodegen(m, owner):
|
if owner != m.module.position and moduleOpenForCodegen(m.g.graph, FileIndex owner):
|
||||||
# make sure the type info is created in the owner module
|
# make sure the type info is created in the owner module
|
||||||
discard genTypeInfoV2(m.g.modules[owner], origType, info)
|
discard genTypeInfoV2(m.g.modules[owner], origType, info)
|
||||||
# reference the type info as extern here
|
# reference the type info as extern here
|
||||||
|
|
@ -1463,7 +1460,7 @@ proc genTypeInfoV1(m: BModule, t: PType; info: TLineInfo): Rope =
|
||||||
return prefixTI.rope & result & ")".rope
|
return prefixTI.rope & result & ")".rope
|
||||||
|
|
||||||
var owner = t.skipTypes(typedescPtrs).itemId.module
|
var owner = t.skipTypes(typedescPtrs).itemId.module
|
||||||
if owner != m.module.position and moduleOpenForCodegen(m, owner):
|
if owner != m.module.position and moduleOpenForCodegen(m.g.graph, FileIndex owner):
|
||||||
# make sure the type info is created in the owner module
|
# make sure the type info is created in the owner module
|
||||||
discard genTypeInfoV1(m.g.modules[owner], origType, info)
|
discard genTypeInfoV1(m.g.modules[owner], origType, info)
|
||||||
# reference the type info as extern here
|
# reference the type info as extern here
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ when not defined(leanCompiler):
|
||||||
|
|
||||||
import strutils except `%` # collides with ropes.`%`
|
import strutils except `%` # collides with ropes.`%`
|
||||||
|
|
||||||
|
from ic / ic import ModuleBackendFlag
|
||||||
from modulegraphs import ModuleGraph, PPassContext
|
from modulegraphs import ModuleGraph, PPassContext
|
||||||
from lineinfos import
|
from lineinfos import
|
||||||
warnGcMem, errXMustBeCompileTime, hintDependency, errGenerated, errCannotOpenFile
|
warnGcMem, errXMustBeCompileTime, hintDependency, errGenerated, errCannotOpenFile
|
||||||
|
|
@ -1501,6 +1502,33 @@ proc genMainProc(m: BModule) =
|
||||||
if m.config.cppCustomNamespace.len > 0:
|
if m.config.cppCustomNamespace.len > 0:
|
||||||
m.s[cfsProcs].add openNamespaceNim(m.config.cppCustomNamespace)
|
m.s[cfsProcs].add openNamespaceNim(m.config.cppCustomNamespace)
|
||||||
|
|
||||||
|
proc registerInitProcs*(g: BModuleList; m: PSym; flags: set[ModuleBackendFlag]) =
|
||||||
|
## Called from the IC backend.
|
||||||
|
if HasDatInitProc in flags:
|
||||||
|
let datInit = getSomeNameForModule(m) & "DatInit000"
|
||||||
|
g.mainModProcs.addf("N_LIB_PRIVATE N_NIMCALL(void, $1)(void);$N", [datInit])
|
||||||
|
g.mainDatInit.addf("\t$1();$N", [datInit])
|
||||||
|
if HasModuleInitProc in flags:
|
||||||
|
let init = getSomeNameForModule(m) & "Init000"
|
||||||
|
g.mainModProcs.addf("N_LIB_PRIVATE N_NIMCALL(void, $1)(void);$N", [init])
|
||||||
|
let initCall = "\t$1();$N" % [init]
|
||||||
|
if sfMainModule in m.flags:
|
||||||
|
g.mainModInit.add(initCall)
|
||||||
|
elif sfSystemModule in m.flags:
|
||||||
|
g.mainDatInit.add(initCall) # systemInit must called right after systemDatInit if any
|
||||||
|
else:
|
||||||
|
g.otherModsInit.add(initCall)
|
||||||
|
|
||||||
|
proc whichInitProcs*(m: BModule): set[ModuleBackendFlag] =
|
||||||
|
# called from IC.
|
||||||
|
result = {}
|
||||||
|
if m.hcrOn or m.preInitProc.s(cpsInit).len > 0 or m.preInitProc.s(cpsStmts).len > 0:
|
||||||
|
result.incl HasModuleInitProc
|
||||||
|
for i in cfsTypeInit1..cfsDynLibInit:
|
||||||
|
if m.s[i].len != 0:
|
||||||
|
result.incl HasDatInitProc
|
||||||
|
break
|
||||||
|
|
||||||
proc registerModuleToMain(g: BModuleList; m: BModule) =
|
proc registerModuleToMain(g: BModuleList; m: BModule) =
|
||||||
let
|
let
|
||||||
init = m.getInitName
|
init = m.getInitName
|
||||||
|
|
@ -1595,6 +1623,7 @@ proc genDatInitCode(m: BModule) =
|
||||||
|
|
||||||
if moduleDatInitRequired:
|
if moduleDatInitRequired:
|
||||||
m.s[cfsDatInitProc].add(prc)
|
m.s[cfsDatInitProc].add(prc)
|
||||||
|
#rememberFlag(m.g.graph, m.module, HasDatInitProc)
|
||||||
|
|
||||||
# Very similar to the contents of symInDynamicLib - basically only the
|
# Very similar to the contents of symInDynamicLib - basically only the
|
||||||
# things needed for the hot code reloading runtime procs to be loaded
|
# things needed for the hot code reloading runtime procs to be loaded
|
||||||
|
|
@ -1725,6 +1754,7 @@ proc genInitCode(m: BModule) =
|
||||||
|
|
||||||
if moduleInitRequired or sfMainModule in m.module.flags:
|
if moduleInitRequired or sfMainModule in m.module.flags:
|
||||||
m.s[cfsInitProc].add(prc)
|
m.s[cfsInitProc].add(prc)
|
||||||
|
#rememberFlag(m.g.graph, m.module, HasModuleInitProc)
|
||||||
|
|
||||||
genDatInitCode(m)
|
genDatInitCode(m)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,11 @@ proc generateCodeForModule(g: ModuleGraph; m: var LoadedModule; alive: var Alive
|
||||||
cgen.genTopLevelStmt(bmod, n)
|
cgen.genTopLevelStmt(bmod, n)
|
||||||
|
|
||||||
finalCodegenActions(g, bmod, newNodeI(nkStmtList, m.module.info))
|
finalCodegenActions(g, bmod, newNodeI(nkStmtList, m.module.info))
|
||||||
|
m.fromDisk.backendFlags = cgen.whichInitProcs(bmod)
|
||||||
|
|
||||||
proc replayTypeInfo(g: ModuleGraph; m: var LoadedModule; origin: FileIndex) =
|
proc replayTypeInfo(g: ModuleGraph; m: var LoadedModule; origin: FileIndex) =
|
||||||
for x in mitems(m.fromDisk.emittedTypeInfo):
|
for x in mitems(m.fromDisk.emittedTypeInfo):
|
||||||
|
#echo "found type ", x, " for file ", int(origin)
|
||||||
g.emittedTypeInfo[x] = origin
|
g.emittedTypeInfo[x] = origin
|
||||||
|
|
||||||
proc addFileToLink(config: ConfigRef; m: PSym) =
|
proc addFileToLink(config: ConfigRef; m: PSym) =
|
||||||
|
|
@ -112,12 +114,16 @@ proc generateCode*(g: ModuleGraph) =
|
||||||
resetForBackend(g)
|
resetForBackend(g)
|
||||||
var alive = computeAliveSyms(g.packed, g.config)
|
var alive = computeAliveSyms(g.packed, g.config)
|
||||||
|
|
||||||
|
when false:
|
||||||
|
for i in 0..high(g.packed):
|
||||||
|
echo i, " is of status ", g.packed[i].status, " ", toFullPath(g.config, FileIndex(i))
|
||||||
|
|
||||||
for i in 0..high(g.packed):
|
for i in 0..high(g.packed):
|
||||||
# case statement here to enforce exhaustive checks.
|
# case statement here to enforce exhaustive checks.
|
||||||
case g.packed[i].status
|
case g.packed[i].status
|
||||||
of undefined:
|
of undefined:
|
||||||
discard "nothing to do"
|
discard "nothing to do"
|
||||||
of loading:
|
of loading, stored:
|
||||||
assert false
|
assert false
|
||||||
of storing, outdated:
|
of storing, outdated:
|
||||||
generateCodeForModule(g, g.packed[i], alive)
|
generateCodeForModule(g, g.packed[i], alive)
|
||||||
|
|
@ -133,3 +139,7 @@ proc generateCode*(g: ModuleGraph) =
|
||||||
else:
|
else:
|
||||||
addFileToLink(g.config, g.packed[i].module)
|
addFileToLink(g.config, g.packed[i].module)
|
||||||
replayTypeInfo(g, g.packed[i], FileIndex(i))
|
replayTypeInfo(g, g.packed[i], FileIndex(i))
|
||||||
|
|
||||||
|
if g.backend == nil:
|
||||||
|
g.backend = cgendata.newModuleList(g)
|
||||||
|
registerInitProcs(BModuleList(g.backend), g.packed[i].module, g.packed[i].fromDisk.backendFlags)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ type
|
||||||
options: TOptions
|
options: TOptions
|
||||||
globalOptions: TGlobalOptions
|
globalOptions: TGlobalOptions
|
||||||
|
|
||||||
|
ModuleBackendFlag* = enum
|
||||||
|
HasDatInitProc
|
||||||
|
HasModuleInitProc
|
||||||
|
|
||||||
PackedModule* = object ## the parts of a PackedEncoder that are part of the .rod file
|
PackedModule* = object ## the parts of a PackedEncoder that are part of the .rod file
|
||||||
definedSymbols: string
|
definedSymbols: string
|
||||||
includes: seq[(LitId, string)] # first entry is the module filename itself
|
includes: seq[(LitId, string)] # first entry is the module filename itself
|
||||||
|
|
@ -43,6 +47,7 @@ type
|
||||||
enumToStringProcs*: seq[(PackedItemId, PackedItemId)]
|
enumToStringProcs*: seq[(PackedItemId, PackedItemId)]
|
||||||
|
|
||||||
emittedTypeInfo*: seq[string]
|
emittedTypeInfo*: seq[string]
|
||||||
|
backendFlags*: set[ModuleBackendFlag]
|
||||||
|
|
||||||
sh*: Shared
|
sh*: Shared
|
||||||
cfg: PackedConfig
|
cfg: PackedConfig
|
||||||
|
|
@ -556,6 +561,9 @@ proc loadRodFile*(filename: AbsoluteFile; m: var PackedModule; config: ConfigRef
|
||||||
loadSeqSection enumToStringProcsSection, m.enumToStringProcs
|
loadSeqSection enumToStringProcsSection, m.enumToStringProcs
|
||||||
loadSeqSection typeInfoSection, m.emittedTypeInfo
|
loadSeqSection typeInfoSection, m.emittedTypeInfo
|
||||||
|
|
||||||
|
f.loadSection backendFlagsSection
|
||||||
|
f.loadPrim m.backendFlags
|
||||||
|
|
||||||
close(f)
|
close(f)
|
||||||
result = f.err
|
result = f.err
|
||||||
|
|
||||||
|
|
@ -619,6 +627,9 @@ proc saveRodFile*(filename: AbsoluteFile; encoder: var PackedEncoder; m: var Pac
|
||||||
storeSeqSection enumToStringProcsSection, m.enumToStringProcs
|
storeSeqSection enumToStringProcsSection, m.enumToStringProcs
|
||||||
storeSeqSection typeInfoSection, m.emittedTypeInfo
|
storeSeqSection typeInfoSection, m.emittedTypeInfo
|
||||||
|
|
||||||
|
f.storeSection backendFlagsSection
|
||||||
|
f.storePrim m.backendFlags
|
||||||
|
|
||||||
close(f)
|
close(f)
|
||||||
encoder.disable()
|
encoder.disable()
|
||||||
if f.err != ok:
|
if f.err != ok:
|
||||||
|
|
@ -646,7 +657,8 @@ type
|
||||||
storing, # state is strictly for stress-testing purposes
|
storing, # state is strictly for stress-testing purposes
|
||||||
loading,
|
loading,
|
||||||
loaded,
|
loaded,
|
||||||
outdated
|
outdated,
|
||||||
|
stored # store is complete, no further additions possible
|
||||||
|
|
||||||
LoadedModule* = object
|
LoadedModule* = object
|
||||||
status*: ModuleStatus
|
status*: ModuleStatus
|
||||||
|
|
@ -673,7 +685,7 @@ proc toFileIndexCached*(c: var PackedDecoder; g: PackedModuleGraph; thisModule:
|
||||||
|
|
||||||
proc translateLineInfo(c: var PackedDecoder; g: var PackedModuleGraph; thisModule: int;
|
proc translateLineInfo(c: var PackedDecoder; g: var PackedModuleGraph; thisModule: int;
|
||||||
x: PackedLineInfo): TLineInfo =
|
x: PackedLineInfo): TLineInfo =
|
||||||
assert g[thisModule].status in {loaded, storing}
|
assert g[thisModule].status in {loaded, storing, stored}
|
||||||
result = TLineInfo(line: x.line, col: x.col,
|
result = TLineInfo(line: x.line, col: x.col,
|
||||||
fileIndex: toFileIndexCached(c, g, thisModule, x.file))
|
fileIndex: toFileIndexCached(c, g, thisModule, x.file))
|
||||||
|
|
||||||
|
|
@ -806,7 +818,7 @@ proc loadSym(c: var PackedDecoder; g: var PackedModuleGraph; thisModule: int; s:
|
||||||
result = nil
|
result = nil
|
||||||
else:
|
else:
|
||||||
let si = moduleIndex(c, g, thisModule, s)
|
let si = moduleIndex(c, g, thisModule, s)
|
||||||
assert g[si].status in {loaded, storing}
|
assert g[si].status in {loaded, storing, stored}
|
||||||
if not g[si].symsInit:
|
if not g[si].symsInit:
|
||||||
g[si].symsInit = true
|
g[si].symsInit = true
|
||||||
setLen g[si].syms, g[si].fromDisk.sh.syms.len
|
setLen g[si].syms, g[si].fromDisk.sh.syms.len
|
||||||
|
|
@ -852,7 +864,7 @@ proc loadType(c: var PackedDecoder; g: var PackedModuleGraph; thisModule: int; t
|
||||||
result = nil
|
result = nil
|
||||||
else:
|
else:
|
||||||
let si = moduleIndex(c, g, thisModule, t)
|
let si = moduleIndex(c, g, thisModule, t)
|
||||||
assert g[si].status in {loaded, storing}
|
assert g[si].status in {loaded, storing, stored}
|
||||||
assert t.item > 0
|
assert t.item > 0
|
||||||
|
|
||||||
if not g[si].typesInit:
|
if not g[si].typesInit:
|
||||||
|
|
@ -950,7 +962,7 @@ proc needsRecompile(g: var PackedModuleGraph; conf: ConfigRef; cache: IdentCache
|
||||||
of loading, loaded:
|
of loading, loaded:
|
||||||
# For loading: Assume no recompile is required.
|
# For loading: Assume no recompile is required.
|
||||||
result = false
|
result = false
|
||||||
of outdated, storing:
|
of outdated, storing, stored:
|
||||||
result = true
|
result = true
|
||||||
|
|
||||||
proc moduleFromRodFile*(g: var PackedModuleGraph; conf: ConfigRef; cache: IdentCache;
|
proc moduleFromRodFile*(g: var PackedModuleGraph; conf: ConfigRef; cache: IdentCache;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ type
|
||||||
methodsPerTypeSection
|
methodsPerTypeSection
|
||||||
enumToStringProcsSection
|
enumToStringProcsSection
|
||||||
typeInfoSection # required by the backend
|
typeInfoSection # required by the backend
|
||||||
|
backendFlagsSection
|
||||||
aliveSymsSection # beware, this is stored in a `.alivesyms` file.
|
aliveSymsSection # beware, this is stored in a `.alivesyms` file.
|
||||||
|
|
||||||
RodFileError* = enum
|
RodFileError* = enum
|
||||||
|
|
|
||||||
|
|
@ -455,11 +455,25 @@ proc getModule*(g: ModuleGraph; fileIdx: FileIndex): PSym =
|
||||||
elif fileIdx.int32 < g.ifaces.len:
|
elif fileIdx.int32 < g.ifaces.len:
|
||||||
result = g.ifaces[fileIdx.int32].module
|
result = g.ifaces[fileIdx.int32].module
|
||||||
|
|
||||||
|
proc moduleOpenForCodegen*(g: ModuleGraph; m: FileIndex): bool {.inline.} =
|
||||||
|
if g.config.symbolFiles == disabledSf:
|
||||||
|
result = true
|
||||||
|
else:
|
||||||
|
result = g.packed[m.int32].status notin {undefined, stored, loaded}
|
||||||
|
|
||||||
proc rememberEmittedTypeInfo*(g: ModuleGraph; m: FileIndex; ti: string) =
|
proc rememberEmittedTypeInfo*(g: ModuleGraph; m: FileIndex; ti: string) =
|
||||||
#assert(not isCachedModule(g, m.int32))
|
#assert(not isCachedModule(g, m.int32))
|
||||||
if g.config.symbolFiles != disabledSf:
|
if g.config.symbolFiles != disabledSf:
|
||||||
#assert g.encoders[m.int32].isActive
|
#assert g.encoders[m.int32].isActive
|
||||||
|
assert g.packed[m.int32].status != stored
|
||||||
g.packed[m.int32].fromDisk.emittedTypeInfo.add ti
|
g.packed[m.int32].fromDisk.emittedTypeInfo.add ti
|
||||||
|
#echo "added typeinfo ", m.int32, " ", ti, " suspicious ", not g.encoders[m.int32].isActive
|
||||||
|
|
||||||
|
proc rememberFlag*(g: ModuleGraph; m: PSym; flag: ModuleBackendFlag) =
|
||||||
|
if g.config.symbolFiles != disabledSf:
|
||||||
|
#assert g.encoders[m.int32].isActive
|
||||||
|
assert g.packed[m.position].status != stored
|
||||||
|
g.packed[m.position].fromDisk.backendFlags.incl flag
|
||||||
|
|
||||||
proc closeRodFile*(g: ModuleGraph; m: PSym) =
|
proc closeRodFile*(g: ModuleGraph; m: PSym) =
|
||||||
if g.config.symbolFiles in {readOnlySf, v2Sf}:
|
if g.config.symbolFiles in {readOnlySf, v2Sf}:
|
||||||
|
|
@ -469,6 +483,8 @@ proc closeRodFile*(g: ModuleGraph; m: PSym) =
|
||||||
let mint = m.position
|
let mint = m.position
|
||||||
saveRodFile(toRodFile(g.config, AbsoluteFile toFullPath(g.config, FileIndex(mint))),
|
saveRodFile(toRodFile(g.config, AbsoluteFile toFullPath(g.config, FileIndex(mint))),
|
||||||
g.encoders[mint], g.packed[mint].fromDisk)
|
g.encoders[mint], g.packed[mint].fromDisk)
|
||||||
|
g.packed[mint].status = stored
|
||||||
|
|
||||||
elif g.config.symbolFiles == stressTest:
|
elif g.config.symbolFiles == stressTest:
|
||||||
# debug code, but maybe a good idea for production? Could reduce the compiler's
|
# debug code, but maybe a good idea for production? Could reduce the compiler's
|
||||||
# memory consumption considerably at the cost of more loads from disk.
|
# memory consumption considerably at the cost of more loads from disk.
|
||||||
|
|
|
||||||
21
koch.nim
21
koch.nim
|
|
@ -458,8 +458,8 @@ proc temp(args: string) =
|
||||||
inc i
|
inc i
|
||||||
|
|
||||||
let d = getAppDir()
|
let d = getAppDir()
|
||||||
var output = d / "compiler" / "nim".exe
|
let output = d / "compiler" / "nim".exe
|
||||||
var finalDest = d / "bin" / "nim_temp".exe
|
let finalDest = d / "bin" / "nim_temp".exe
|
||||||
# 125 is the magic number to tell git bisect to skip the current commit.
|
# 125 is the magic number to tell git bisect to skip the current commit.
|
||||||
var (bootArgs, programArgs) = splitArgs(args)
|
var (bootArgs, programArgs) = splitArgs(args)
|
||||||
if "doc" notin programArgs and
|
if "doc" notin programArgs and
|
||||||
|
|
@ -483,6 +483,22 @@ proc xtemp(cmd: string) =
|
||||||
finally:
|
finally:
|
||||||
copyExe(d / "bin" / "nim_backup".exe, d / "bin" / "nim".exe)
|
copyExe(d / "bin" / "nim_backup".exe, d / "bin" / "nim".exe)
|
||||||
|
|
||||||
|
proc icTest(args: string) =
|
||||||
|
temp("")
|
||||||
|
let inp = os.parseCmdLine(args)[0]
|
||||||
|
let content = readFile(inp)
|
||||||
|
let nimExe = getAppDir() / "bin" / "nim_temp".exe
|
||||||
|
var i = 0
|
||||||
|
for fragment in content.split("#!EDIT!#"):
|
||||||
|
let file = inp.replace(".nim", "_temp.nim")
|
||||||
|
writeFile(file, fragment)
|
||||||
|
var cmd = nimExe & " cpp --ic:on --listcmd "
|
||||||
|
if i == 0:
|
||||||
|
cmd.add "-f "
|
||||||
|
cmd.add quoteShell(file)
|
||||||
|
exec(cmd)
|
||||||
|
inc i
|
||||||
|
|
||||||
proc buildDrNim(args: string) =
|
proc buildDrNim(args: string) =
|
||||||
if not dirExists("dist/nimz3"):
|
if not dirExists("dist/nimz3"):
|
||||||
exec("git clone https://github.com/zevv/nimz3.git dist/nimz3")
|
exec("git clone https://github.com/zevv/nimz3.git dist/nimz3")
|
||||||
|
|
@ -705,6 +721,7 @@ when isMainModule:
|
||||||
of "fusion":
|
of "fusion":
|
||||||
let suffix = if latest: HeadHash else: FusionStableHash
|
let suffix = if latest: HeadHash else: FusionStableHash
|
||||||
exec("nimble install -y fusion@$#" % suffix)
|
exec("nimble install -y fusion@$#" % suffix)
|
||||||
|
of "ic": icTest(op.cmdLineRest)
|
||||||
else: showHelp()
|
else: showHelp()
|
||||||
break
|
break
|
||||||
of cmdEnd: break
|
of cmdEnd: break
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
discard """
|
discard """
|
||||||
output: "bar"
|
output: "bar"
|
||||||
disabled: "true"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tables
|
import tables
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue