#
#
#           The Nim Compiler
#        (c) Copyright 2021 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## Integrity checking for a set of .rod files.
## The set must cover a complete Nim project.

import sets

when defined(nimPreviewSlimSystem):
  import std/assertions

import ".." / [ast, modulegraphs]
import packed_ast, bitabs, ic

type
  CheckedContext = object
    g: ModuleGraph
    thisModule: int32
    checkedSyms: HashSet[ItemId]
    checkedTypes: HashSet[ItemId]

proc checkType(c: var CheckedContext; typeId: PackedItemId)
proc checkForeignSym(c: var CheckedContext; symId: PackedItemId)
proc checkNode(c: var CheckedContext; tree: PackedTree; n: NodePos)

proc checkTypeObj(c: var CheckedContext; typ: PackedType) =
  for child in typ.types:
    checkType(c, child)
  if typ.n != emptyNodeId:
    checkNode(c, c.g.packed[c.thisModule].fromDisk.bodies, NodePos typ.n)
  if typ.sym != nilItemId:
    checkForeignSym(c, typ.sym)
  if typ.owner != nilItemId:
    checkForeignSym(c, typ.owner)
  checkType(c, typ.typeInst)

proc checkType(c: var CheckedContext; typeId: PackedItemId) =
  if typeId == nilItemId: return
  let itemId = translateId(typeId, c.g.packed, c.thisModule, c.g.config)
  if not c.checkedTypes.containsOrIncl(itemId):
    let oldThisModule = c.thisModule
    c.thisModule = itemId.module
    checkTypeObj c, c.g.packed[itemId.module].fromDisk.types[itemId.item]
    c.thisModule = oldThisModule

proc checkSym(c: var CheckedContext; s: PackedSym) =
  if s.name != LitId(0):
    assert c.g.packed[c.thisModule].fromDisk.strings.hasLitId s.name
  checkType c, s.typ
  if s.ast != emptyNodeId:
    checkNode(c, c.g.packed[c.thisModule].fromDisk.bodies, NodePos s.ast)
  if s.owner != nilItemId:
    checkForeignSym(c, s.owner)

proc checkLocalSym(c: var CheckedContext; item: int32) =
  let itemId = ItemId(module: c.thisModule, item: item)
  if not c.checkedSyms.containsOrIncl(itemId):
    checkSym c, c.g.packed[c.thisModule].fromDisk.syms[item]

proc checkForeignSym(c: var CheckedContext; symId: PackedItemId) =
  let itemId = translateId(symId, c.g.packed, c.thisModule, c.g.config)
  if not c.checkedSyms.containsOrIncl(itemId):
    let oldThisModule = c.thisModule
    c.thisModule = itemId.module
    checkSym c, c.g.packed[itemId.module].fromDisk.syms[itemId.item]
    c.thisModule = oldThisModule

proc checkNode(c: var CheckedContext; tree: PackedTree; n: NodePos) =
  if tree[n.int].typeId != nilItemId:
    checkType(c, tree[n.int].typeId)
  case n.kind
  of nkEmpty, nkNilLit, nkType, nkNilRodNode:
    discard
  of nkIdent:
    assert c.g.packed[c.thisModule].fromDisk.strings.hasLitId n.litId
  of nkSym:
    checkLocalSym(c, tree.nodes[n.int].operand)
  of directIntLit:
    discard
  of externIntLit, nkFloatLit..nkFloat128Lit:
    assert c.g.packed[c.thisModule].fromDisk.numbers.hasLitId n.litId
  of nkStrLit..nkTripleStrLit:
    assert c.g.packed[c.thisModule].fromDisk.strings.hasLitId n.litId
  of nkModuleRef:
    let (n1, n2) = sons2(tree, n)
    assert n1.kind == nkInt32Lit
    assert n2.kind == nkInt32Lit
    checkForeignSym(c, PackedItemId(module: n1.litId, item: tree.nodes[n2.int].operand))
  else:
    for n0 in sonsReadonly(tree, n):
      checkNode(c, tree, n0)

proc checkTree(c: var CheckedContext; t: PackedTree) =
  for p in allNodes(t): checkNode(c, t, p)

proc checkLocalSymIds(c: var CheckedContext; m: PackedModule; symIds: seq[int32]) =
  for symId in symIds:
    assert symId >= 0 and symId < m.syms.len, $symId & " " & $m.syms.len

proc checkModule(c: var CheckedContext; m: PackedModule) =
  # We check that:
  # - Every symbol references existing types and symbols.
  # - Every tree node references existing types and symbols.
  for i in 0..high(m.syms):
    checkLocalSym c, int32(i)

  checkTree c, m.toReplay
  checkTree c, m.topLevel

  for e in m.exports:
    assert e[1] >= 0 and e[1] < m.syms.len
    assert e[0] == m.syms[e[1]].name

  for e in m.compilerProcs:
    assert e[1] >= 0 and e[1] < m.syms.len
    assert e[0] == m.syms[e[1]].name

  checkLocalSymIds c, m, m.converters
  checkLocalSymIds c, m, m.methods
  checkLocalSymIds c, m, m.trmacros
  checkLocalSymIds c, m, m.pureEnums
  #[
    To do: Check all these fields:

    reexports*: seq[(LitId, PackedItemId)]
    macroUsages*: seq[(PackedItemId, PackedLineInfo)]

    typeInstCache*: seq[(PackedItemId, PackedItemId)]
    procInstCache*: seq[PackedInstantiation]
    attachedOps*: seq[(TTypeAttachedOp, PackedItemId, PackedItemId)]
    methodsPerType*: seq[(PackedItemId, int, PackedItemId)]
    enumToStringProcs*: seq[(PackedItemId, PackedItemId)]
  ]#

proc checkIntegrity*(g: ModuleGraph) =
  var c = CheckedContext(g: g)
  for i in 0..high(g.packed):
    # case statement here to enforce exhaustive checks.
    case g.packed[i].status
    of undefined:
      discard "nothing to do"
    of loading:
      assert false, "cannot check integrity: Module still loading"
    of stored, storing, outdated, loaded:
      c.thisModule = int32 i
      checkModule(c, g.packed[i].fromDisk)

