#       Nimony
# (c) Copyright 2024 Andreas Rumpf
#
# See the file "license.txt", included in this
# distribution, for details about the copyright.

##[

Inserts `haddr` and `hderef` nodes for the code generators or analysis passes that
need to care (move analyser, etc.).

Expressions
===========

There are 4 cases:

1. `var T` to `var T` transfer: Nothing to do.
2. `var T` to `T` transfer: Insert HiddenDeref instruction.
3. `T` to `var T` transfer: Insert HiddenAddr instruction.
4. `T` to `T` transfer: Nothing to do.

Now also does some simple checks for `raise` statements:
- Only a routine marked as `.raises` can call another routine marked as `.raises`.

Also adds lifetime tracking hooks to type declarations so that Hexer can find them
easily.

Also enforces .noSideEffect contexts.
Also checks that accesses to locals that indicate a closure are within an explicit
`.closure` proc. `.closure` is not inferred anymore by the compiler as it's fundamentally
incompatible with Nim's type checking: Type checking influences template expansions which
can lead to accessed locals which can lead to closures which we assumed not to exist for
type checking purposes.

We can then improve compat with Nim 2 by making all inner procs `.closure` by default.

]##

import std / [assertions, tables, hashes, sets]

when defined(nimony):
  {.feature: "lenientnils".}

include ".." / lib / nifprelude
include ".." / lib / compat2

import ".." / models / tags
import ".." / hexer / lifter
import nimony_model, programs, decls, typenav, sembasics, reporters, renderer, typeprops, vtables_frontend, semdata

type
  Expects = enum
    WantT
    WantTButSkipDeref
    WantVarT
    WantVarTResult
    WantMutableT  # `var openArray` does need derefs but mutable locations regardless
    WantForwarding

  RoutineProp = enum
    CanRaise, IsNoSideEffect, IsClosure

  CurrentRoutine = object
    returnExpects: Expects
    firstParamKind: TypeKind
    props: set[RoutineProp]
    firstParam: SymId
    resultSym: SymId
    dangerousLocations: Table[SymId, PackedLineInfo]

  Context = object
    dest: TokenBuf
    r: CurrentRoutine
    typeCache: TypeCache
    hooks: Table[SymId, HooksPerType]
    classes: semdata.Classes # class entries with methods for vtables
    lifter: ref LiftingCtx
    typeSymBufs: seq[TokenBuf] # keeps Symbol cursors alive for lifter

proc takeToken(c: var Context; n: var Cursor) {.inline.} =
  c.dest.add n
  inc n

proc takeParRi(c: var Context; n: var Cursor) =
  if n.kind == ParRi:
    c.dest.add n
    inc n
  else:
    bug "expected ')', but got: ", n

proc rootOf(n: Cursor; allowIndirection = false): SymId =
  var n = n
  while true:
    case n.exprKind
    of DotX, AtX, ArratX, TupatX, ParX:
      inc n
    of PatX, DdotX:
      # not protected from mutation
      if allowIndirection:
        inc n
      else:
        break
    of DconvX:
      inc n
      skip n # skip the type
    of BaseobjX:
      inc n
      skip n # skip intlit
      skip n # skip type
    else: break
  case n.kind
  of Symbol, SymbolDef:
    result = n.symId
  else:
    result = NoSymId

proc isAddressable*(n: Cursor): bool =
  ## Addressable means that we can take the address of the expression.
  let s = rootOf(n, allowIndirection = true)
  if s != NoSymId:
    let res = tryLoadSym(s)
    assert res.status == LacksNothing
    let local = asLocal(res.decl)
    result = local.kind in {ParamY, LetY, ResultY, VarY, CursorY, PatternvarY, ConstY, GletY, TletY, GvarY, TvarY}
    # Assignments to `ConstY` are prevented later.
  else:
    result = false

proc tr(c: var Context; n: var Cursor; e: Expects)

proc trSons(c: var Context; n: var Cursor; e: Expects) =
  if n.kind != ParLe:
    takeToken c, n
  else:
    takeToken c, n
    while n.kind != ParRi:
      tr c, n, e
    takeParRi c, n

proc validBorrowsFrom(c: var Context; n: Cursor): bool =
  # --------------------------------------------------------------------------
  # We need to borrow from a location that is derived from the proc's
  # first parameter.
  # --------------------------------------------------------------------------
  var n = n
  var someIndirection = false
  while true:
    case n.exprKind
    of DotX, AtX, ArratX, TupatX, ParX:
      inc n
    of HderefX, HaddrX, DerefX, AddrX, DdotX, PatX:
      inc n
      someIndirection = true
    of DconvX, ConvX, CastX:
      inc n
      skip n # skip the type
    of BaseobjX:
      inc n
      skip n # skip type
      skip n # skip intlit
    of ExprX:
      inc n
      while true:
        if isLastSon(n): break
        skip n
    of CallKinds:
      inc n
      let fn = n
      skip n # skip the `fn`
      if n.kind != ParRi:
        var fnType = skipProcTypeToParams(getType(c.typeCache, fn))
        assert fnType.isParamsTag
        inc fnType
        let firstParam = asLocal(fnType)
        if firstParam.kind == ParamY:
          let mightForward = firstParam.typ.typeKind in {MutT, OutT, LentT}
          if not mightForward:
            someIndirection = true
        # borrowing only work from first parameters:
        discard "n already at the correct position"
      else:
        break
    else:
      break
  if n.kind == Symbol:
    let res = tryLoadSym(n.symId)
    assert res.status == LacksNothing
    # XXX Is this check really reliable for local symbols
    # that are not guaranteed to be unique?
    if res.decl.tagEnum == ParamTagId and n.symId == c.r.firstParam:
      # There is a difference between
      # proc forward(x: var int): var int = x
      # and
      # proc access(x: Table): var int = x[].field[0]
      # and we do not try to hide it!
      result = c.r.firstParamKind in {MutT, OutT, LentT} or someIndirection
    else:
      result = false
  else:
    result = false

proc skipToRoot(n: Cursor): Cursor =
  var n = n
  while true:
    case n.exprKind
    of DotX, AtX, ArratX, TupatX, ParX:
      inc n
    of DconvX, HconvX, ConvX, CastX:
      inc n
      skip n
    of BaseobjX:
      inc n
      skip n # skip intlit
      skip n # skip type
    of CallKinds:
      inc n
      skip n # skip the `fn`
      if n.kind != ParRi:
        # borrowing only work from first parameters:
        discard "n already at the correct position"
      else:
        break
    else:
      break
  result = n

proc borrowsFromReadonly(c: var Context; n: Cursor; allowLet = false): bool =
  let n = skipToRoot(n)
  if n.kind == Symbol:
    let res = tryLoadSym(n.symId)
    assert res.status == LacksNothing
    let local = asLocal(res.decl)
    case local.kind
    of ConstY:
      result = true
    of LetY, GletY, TletY:
      let tk = local.typ.typeKind
      if allowLet:
        result = tk == LentT # see VarY case
      else:
        result = tk notin {MutT, OutT, LentT}
        if result and isViewType(local.typ):
          # Special rule to make `toOpenArray` work:
          result = borrowsFromReadonly(c, local.val)
    of VarY, GvarY, TvarY:
      result = local.typ.typeKind == LentT
    of PatternvarY:
      result = borrowsFromReadonly(c, local.val)
    of ParamY:
      result = local.typ.typeKind notin {MutT, OutT, LentT, SinkT}
    else:
      result = false
  elif n.kind in {StringLit, IntLit, UIntLit, FloatLit, CharLit} or
       n.exprKind in {SufX, OconstrX, NewobjX, AconstrX}:
    result = true
  else:
    result = false

type
  LvalueStatus = enum
    Valid
    InvalidBorrow
    LocationIsConst

proc checkTupleConstrBorrowing(c: var Context; n: Cursor): LvalueStatus =
  result = Valid
  var n = n
  inc n

  var typ = n
  assert typ.typeKind == TupleT
  inc typ
  skip n
  while n.kind != ParRi:
    let fieldType = getTupleFieldType(typ)
    skip typ
    let isKv = n.substructureKind == KvU
    if isKv:
      inc n
      skip n # skip key
    if fieldType.typeKind in {MutT, LentT, OutT}:
      if not validBorrowsFrom(c, n):
        buildLocalErr(c.dest, n.info, "cannot borrow from " & asNimCode(n))
        return InvalidBorrow

    skip n

    if isKv:
      skipParRi(n)

proc isResultUsage(c: Context; n: Cursor): bool {.inline.} =
  result = false
  if n.kind == Symbol:
    result = n.symId == c.r.resultSym

proc trReturn(c: var Context; n: var Cursor) =
  takeToken c, n
  if isResultUsage(c, n):
    takeTree c.dest, n
  else:
    var err = c.r.returnExpects == WantVarTResult and not validBorrowsFrom(c, n)
    if err:
      buildLocalErr(c.dest, n.info, "cannot borrow from " & asNimCode(n))
    else:
      if n.exprKind == TupconstrX:
        if checkTupleConstrBorrowing(c, n) != Valid:
          err = true
      if not err:
        tr c, n, c.r.returnExpects
      else:
        skip n
  takeParRi c, n

proc wantMutable(e: Expects): bool {.inline.} =
  e in {WantVarT, WantVarTResult, WantMutableT}

proc trYield(c: var Context; n: var Cursor) =
  takeToken c, n
  if wantMutable(c.r.returnExpects):
    tr c, n, c.r.returnExpects
  else:
    tr c, n, WantForwarding # c.r.returnExpects
  takeParRi c, n

proc mightBeDangerous(c: var Context; n: Cursor) =
  let root = rootOf(n)
  if root != NoSymId:
    if c.r.dangerousLocations.hasKey(root):
      buildLocalErr c.dest, n.info, "cannot mutate " & asNimCode(n) &
          "; binding created here: " & infoToStr(c.r.dangerousLocations.getOrQuit(root))

proc checkForDangerousLocations(c: var Context; n: var Cursor) =
  template recurse =
    while n.kind != ParRi:
      checkForDangerousLocations c, n
    inc n # skip ParRi

  case n.kind
  of Symbol, UnknownToken, EofToken, DotToken, Ident, SymbolDef, StringLit, CharLit, IntLit, UIntLit, FloatLit:
    inc n
  of ParLe:
    if isDeclarative(n):
      skip n
    elif n.stmtKind == AsgnS:
      inc n
      mightBeDangerous(c, n)
      recurse()
    elif n.exprKind in CallKinds:
      inc n # skip `(call)`
      let orig = n
      var fnType = skipProcTypeToParams(getType(c.typeCache, n))
      skip n # skip `fn`
      assert fnType.isParamsTag
      inc fnType
      while n.kind != ParRi:
        let previousFormalParam = fnType
        let param = takeLocal(fnType, SkipFinalParRi)
        let pk = param.typ.typeKind
        if pk in {MutT, OutT, LentT}:
          mightBeDangerous(c, n)
        elif pk == VarargsT:
          # do not advance formal parameter:
          fnType = previousFormalParam
        skip n
      n = orig
      recurse()
    else:
      inc n
      recurse()
  of ParRi:
    bug "checkForDangerousLocations: ParRi"

proc trProcPragmas(c: var Context; n: var Cursor) =
  # we also need to traverse the `requires` pragmas!
  if n.kind == DotToken:
    takeToken c, n
  else:
    takeToken c, n # pragmas
    while n.kind != ParRi:
      let pk = n.pragmaKind
      if pk == RequiresP:
        tr c, n, WantT
      elif pk == RaisesP:
        c.r.props.incl CanRaise
        takeTree c.dest, n
      elif pk == NoSideEffectP:
        c.r.props.incl IsNoSideEffect
        takeTree c.dest, n
      elif pk == SideEffectP:
        c.r.props.excl IsNoSideEffect
        takeTree c.dest, n
      elif pk == ClosureP:
        c.r.props.incl IsClosure
        takeTree c.dest, n
      else:
        takeTree c.dest, n
    takeParRi c, n

proc trProcDecl(c: var Context; n: var Cursor) =
  let decl = n
  c.typeCache.openScope(ProcScope)
  takeToken c, n
  let symId = n.symId
  var isGeneric = false
  let props = if decl.stmtKind in {FuncS, IteratorS, ConverterS}: {IsNoSideEffect} else: {}
  var r = CurrentRoutine(returnExpects: WantT, props: props)
  swap c.r, r
  for i in 0..<BodyPos:
    if i == TypevarsPos:
      isGeneric = n.substructureKind == TypevarsU
      takeTree c.dest, n
    elif i == ParamsPos:
      c.typeCache.registerParams(symId, decl, n)
      var params = n
      inc params
      let firstParam = asLocal(params)
      if firstParam.kind == ParamY:
        c.r.firstParam = firstParam.name.symId
        c.r.firstParamKind = firstParam.typ.typeKind
      takeTree c.dest, n
    elif i == ProcPragmasPos and not isGeneric:
      trProcPragmas(c, n)
    elif i == ReturnTypePos and n.typeKind in {MutT, OutT, LentT}:
      c.r.returnExpects = WantVarTResult
      takeTree c.dest, n
    else:
      takeTree c.dest, n

  if isGeneric:
    takeTree c.dest, n
    takeParRi c, n
  else:
    var body = n
    tr c, n, c.r.returnExpects
    if c.r.dangerousLocations.len > 0:
      c.dest.shrink c.dest.len - 1
      checkForDangerousLocations c, body
      c.dest.addParRi()
    takeParRi c, n
  swap c.r, r
  c.typeCache.closeScope()

proc callCanRaise(c: var Context; info: PackedLineInfo) {.inline.} =
  if CanRaise notin c.r.props:
    buildLocalErr c.dest, info, "cannot call a routine marked as `.raises` outside of a `try`..`except` block"

proc trCallArgs(c: var Context; n: var Cursor; fnType: Cursor) =
  let info = n.info
  var fnType = skipProcTypeToParams(fnType)
  assert fnType.isParamsTag
  inc fnType
  while n.kind != ParRi:
    var e = WantT
    let previousFormalParam = fnType
    let param = takeLocal(fnType, SkipFinalParRi)
    var pk = param.typ.typeKind
    if pk in {MutT, LentT}:
      var elemType = param.typ
      inc elemType
      if isViewType(elemType):
        e = WantMutableT
      else:
        e = WantVarT
    elif pk == OutT:
      e = WantVarT
    elif pk == VarargsT:
      # do not advance formal parameter:
      fnType = previousFormalParam
    tr c, n, e
  while fnType.kind != ParRi: skip fnType
  inc fnType # skip ParRi
  # skip return type:
  skip fnType
  # now at the pragmas position:
  if hasPragma(fnType, RaisesP):
    callCanRaise(c, info)

proc firstArgIsMutable(c: var Context; n: Cursor): bool =
  assert n.exprKind in CallKinds
  var n = n
  inc n
  assert n.kind != ParRi
  skip n
  if n.kind != ParRi:
    result = not borrowsFromReadonly(c, n)
  else:
    result = false

proc cannotPassToVar(dest: var TokenBuf; info: PackedLineInfo; arg: Cursor) =
  let msg = "cannot pass " & asNimCode(arg) & " to var/out T parameter"
  when defined(debug):
    writeStackTrace()
    echo infoToStr(info) & " Error: " & msg
    quit msg
  dest.buildTree ErrT, info:
    dest.addSubtree arg
    dest.add strToken(pool.strings.getOrIncl(msg), info)

proc trPragmaBlock(c: var Context; n: var Cursor) =
  c.dest.takeToken n # pragmax
  c.dest.takeToken n # pragmas
  if n.pragmaKind == KeepOverflowFlagP:
    c.dest.takeTree n # keepOverflowFlag
    c.dest.takeParRi n # pragmas
    tr(c, n, WantT)
  elif n.pragmaKind == CastP:
    # Look at the inner pragma list of `(cast (pragmas X*))` to decide which
    # routine props to suppress. Only `noSideEffect` clears `IsNoSideEffect`;
    # other recognized cast pragmas (e.g. `uncheckedAssign`) are passed through
    # without touching props.
    var disableNoSideEffect = false
    var inner = n
    inc inner # past `cast`
    if inner.substructureKind == PragmasU:
      inc inner
      while inner.kind != ParRi:
        if inner.pragmaKind == NoSideEffectP:
          disableNoSideEffect = true
        skip inner
    c.dest.takeTree n # cast pragma
    c.dest.takeParRi n # outer pragmas
    let oldProps = c.r.props
    if disableNoSideEffect:
      c.r.props.excl IsNoSideEffect
    tr(c, n, WantT)
    c.r.props = oldProps
  else:
    bug "unknown pragma block: " & toString(n, false)
  c.dest.takeParRi n # pragmax

proc trCall(c: var Context; n: var Cursor; e: Expects; dangerous: var bool) =
  let info = n.info
  let callExpr = n

  var callBuf = createTokenBuf()

  swap c.dest, callBuf
  let head = n.load()
  inc n
  let tt = getType(c.typeCache, n)
  let calleeKind = tt.stmtKind
  let fnType = skipProcTypeToParams(tt)
  assert fnType.isParamsTag
  var retType = fnType
  skip retType
  var pragmas = retType
  skip pragmas
  if IsNoSideEffect in c.r.props:
    if whichEffect(calleeKind, pragmas) == HasSideEffect:
      swap c.dest, callBuf
      buildLocalErr c.dest, n.info, "cannot call a routine with side effects from a `.noSideEffect` context"
      n = callExpr
      skip n
      return

  c.dest.add head # (call)
  tr c, n, WantT # `fn` part of the call

  var needHderef = false
  if retType.typeKind in {MutT, LentT}:
    if e in {WantT, WantForwarding}:
      needHderef = true
      trCallArgs(c, n, fnType)
      takeParRi c, n
    elif e in {WantVarTResult, WantTButSkipDeref} or firstArgIsMutable(c, callExpr):
      trCallArgs(c, n, fnType)
      takeParRi c, n
    elif not dangerous:
      # DANGER-ZONE HERE! Consider: `for d in items(a)` where `a` is immutable.
      # Which is turned to `var d: var T = a[i]`.
      # Wether we can allow this or not depends on whether `d` is used later on for mutations!
      # Thus we mark the binding as "dangerous". In a second pass we then look for the pattern
      # `passedToVar(d)` or `d[] = value` and flag them as errornous.
      dangerous = true
      trCallArgs(c, n, fnType)
      takeParRi c, n
    else:
      while n.kind != ParRi: skip n
      inc n # skip ParRi without emitting into callBuf
      swap c.dest, callBuf # restore original dest; discard partial callBuf
      cannotPassToVar c.dest, info, callExpr
      return
  elif e.wantMutable:
    if isViewType(retType) and firstArgIsMutable(c, callExpr):
      trCallArgs(c, n, fnType)
      takeParRi c, n
    else:
      while n.kind != ParRi: skip n
      inc n # skip ParRi without emitting into callBuf
      swap c.dest, callBuf # restore original dest; discard partial callBuf
      cannotPassToVar c.dest, info, callExpr
      return
  else:
    trCallArgs(c, n, fnType)
    takeParRi c, n

  swap c.dest, callBuf
  if needHderef and c.dest[c.dest.len-1].tag != TagId(HderefTagId):
    c.dest.addParLe(HderefX, info)
    c.dest.add callBuf
    c.dest.addParRi()
  else:
    c.dest.add callBuf

proc trAsgnRhs(c: var Context; le: Cursor; ri: var Cursor; e: Expects) =
  if ri.exprKind in CallKinds:
    var dangerous = false
    trCall c, ri, e, dangerous
    if dangerous:
      let s = rootOf(le)
      if s != NoSymId:
        c.r.dangerousLocations[s] = ri.info
  else:
    tr c, ri, e

proc trAsgn(c: var Context; n: var Cursor) =
  takeToken c, n
  var e = WantT
  var err = Valid
  let le = n
  if isResultUsage(c, le):
    e = c.r.returnExpects
    if e == WantVarTResult:
      tr c, n, e
      if not validBorrowsFrom(c, n):
        err = InvalidBorrow
    else:
      tr c, n, e
      if n.exprKind == TupconstrX:
        if checkTupleConstrBorrowing(c, n) != Valid:
          # already emitted an error:
          skip n
          takeParRi c, n
          return
  elif borrowsFromReadonly(c, n, allowLet=true):
    err = LocationIsConst
  else:
    tr c, n, e
  case err
  of InvalidBorrow:
    buildLocalErr c.dest, n.info, "cannot borrow from " & asNimCode(n)
    skip n
  of LocationIsConst:
    buildLocalErr c.dest, n.info, "cannot mutate expression " & asNimCode(n)
    tr c, n, e
    skip n
    #tr c, n, e
  else:
    trAsgnRhs c, le, n, e
  takeParRi c, n

proc trSonsLocation(c: var Context; n: var Cursor; e: Expects) =
  if n.kind != ParLe:
    takeToken c, n
  elif n.exprKind in {DotX, DdotX}:
    takeToken c, n
    tr c, n, e
    while n.kind != ParRi:
      takeTree c.dest, n
    takeParRi c, n
  else:
    takeToken c, n
    while n.kind != ParRi:
      tr c, n, e
    takeParRi c, n

proc trLocation(c: var Context; n: var Cursor; e: Expects) =
  # Idea: A variable like `x` does not own its value as it can be read multiple times.
  let typ = getType(c.typeCache, n)
  let k = typ.typeKind
  if k in {MutT, OutT, LentT}:
    if e.wantMutable:
      # Consider `fvar(returnsVar(someLet))`: We must not allow this.
      if borrowsFromReadonly(c, n):
        cannotPassToVar c.dest, n.info, n
        skip n
      else:
        trSonsLocation c, n, WantT
    else:
      if (k in {MutT, LentT} and not isViewType(typ.firstSon)) or k == OutT:
        if c.dest[c.dest.len-1].tag == TagId(HderefTagId):
          trSonsLocation c, n, WantT
        else:
          c.dest.addParLe(HderefX, n.info)
          trSonsLocation c, n, WantT
          c.dest.addParRi()
      else:
        trSonsLocation c, n, WantT
  elif e.wantMutable:
    if e == WantVarTResult:
      c.dest.addParLe(HaddrX, n.info)
      if n.kind == Symbol:
        takeToken c, n
      else:
        trSonsLocation c, n, WantT
      c.dest.addParRi()
    elif borrowsFromReadonly(c, n):
      cannotPassToVar c.dest, n.info, n
      skip n
    else:
      c.dest.addParLe(HaddrX, n.info)
      trSonsLocation c, n, WantT
      c.dest.addParRi()
  else:
    trSonsLocation c, n, WantT

proc trLocal(c: var Context; n: var Cursor) =
  let kind = n.symKind
  takeToken c, n
  let name = n
  if kind == ResultY:
    c.r.resultSym = name.symId
  for i in 0..<LocalTypePos:
    takeTree c.dest, n
  let typ = n
  takeTree c.dest, n
  c.typeCache.registerLocal(name.symId, kind, typ)
  if kind == PatternvarY:
    c.dest.addParLe(HaddrX, n.info)
    tr c, n, WantT
    c.dest.addParRi()
  else:
    let e = if typ.typeKind in {OutT, MutT, LentT}: WantVarT else: WantT
    trAsgnRhs c, name, n, e
  takeParRi c, n

proc trStmtListExpr(c: var Context; n: var Cursor; outerE: Expects) =
  takeToken c, n
  while n.kind != ParRi:
    if isLastSon(n):
      tr c, n, outerE
    else:
      tr c, n, WantT
  takeParRi c, n

proc fieldMode(k: TypeKind; outerE: Expects): Expects {.inline.} =
  if k in {MutT, OutT, LentT}:
    (if outerE == WantForwarding: WantVarTResult else: WantVarT)
  else:
    WantT

proc trObjConstr(c: var Context; n: var Cursor; outerE: Expects) =
  takeToken c, n
  let objType = n
  takeTree c.dest, n # type
  while n.kind != ParRi:
    assert n.substructureKind == KvU
    takeToken c, n
    # Look up the *field's declared type* before consuming the key, so
    # `tr` knows whether to insert HderefX/HaddrX coercions for fields
    # that legitimately carry `lent`/`var`/`out`/`mut` qualifiers.
    # `tryLoadSym` does not reliably resolve field symbols (they live
    # inside the owning type decl), so use `typenav.lookupField` which
    # walks the object body.
    var fieldKind: TypeKind = NoType
    if n.kind == Symbol:
      let fieldType = lookupField(c.typeCache, objType, n.symId)
      if not cursorIsNil(fieldType):
        fieldKind = fieldType.typeKind
    takeTree c.dest, n # key
    tr c, n, fieldMode(fieldKind, outerE)
    if n.kind != ParRi:
      # optional inheritance
      takeTree c.dest, n
    takeParRi c, n
  takeParRi c, n

proc trTupleConstr(c: var Context; n: var Cursor; outerE: Expects) =
  takeToken c, n
  var typ = n
  assert typ.typeKind == TupleT
  inc typ
  takeTree c.dest, n # type
  while n.kind != ParRi:
    let fieldType = getTupleFieldType(typ)
    skip typ
    let e = fieldMode(fieldType.typeKind, outerE)
    if n.substructureKind == KvU:
      takeToken c, n
      takeTree c.dest, n # skip key
      tr c, n, e
      takeParRi c, n
    else:
      tr c, n, e
  takeParRi c, n

proc trVarHook(c: var Context; n: var Cursor) =
  takeToken c, n
  tr c, n, WantVarT
  if n.kind != ParRi:
    tr c, n, WantT
  takeParRi c, n

proc trTry(c: var Context; n: var Cursor) =
  takeToken c, n
  var nn = n
  skip nn
  let oldProps = c.r.props
  if nn.substructureKind == ExceptU or nn.substructureKind == FinU:
    c.r.props.incl CanRaise
  # now can raise in the `try` block:
  tr c, n, WantT
  c.r.props = oldProps
  while n.kind != ParRi:
    tr c, n, WantT
  takeParRi c, n

proc trForHelper(c: var Context; n: var Cursor; dangerous: seq[SymId]) =
  takeToken c, n
  if dangerous.len > 0:
    if borrowsFromReadonly(c, n, allowLet=true):
      for s in dangerous:
        c.r.dangerousLocations[s] = n.info
  tr c, n, WantT # iterator call
  trSons c, n, WantT # decls
  trSons c, n, WantT # loop body
  takeParRi c, n

proc trFor(c: var Context; n: var Cursor) =
  #[
  (for
    (call items.0.tem6twvye1
     (cmd toOpenArray.0.tem6twvye1
      (dot c.0 paths.0.tem6twvye1 +0)))
    (unpackflat
     (let :p.0 . .
      (mut string.0.sysvq0asl) .)) ]#
  var nn = n.firstSon
  skip nn # iterator
  case substructureKind(nn)
  of UnpackflatU, UnpacktupU:
    inc nn
    var dangerous: seq[SymId] = @[]
    while nn.kind != ParRi:
      inc nn # LetS etc.
      let s = nn.symId
      for i in 0..<LocalTypePos:
        skip nn
      if nn.typeKind in {OutT, MutT, LentT}:
        dangerous.add(s)
      skip nn, SkipType # type
      skip nn, SkipValue # value
    # now work with `n`, not `nn`
    trForHelper c, n, dangerous
  else:
    bug "illformed for loop"

proc addHookDecls(dest: var TokenBuf; hooks: HooksPerType) =
  for op in low(AttachedOp)..high(AttachedOp):
    let s = hooks.a[op]
    if s != SymId(0):
      dest.addParLe hookToTag(op), NoLineInfo
      dest.addSymUse s, NoLineInfo
      dest.addParRi()

proc addMethodsDecl(dest: var TokenBuf; methods: seq[(string, SymId)]) =
  if methods.len > 0:
    dest.addParLe MethodsP, NoLineInfo
    for (key, sym) in methods:
      dest.addParLe KvU, NoLineInfo
      dest.addStrLit key, NoLineInfo
      dest.addSymUse sym, NoLineInfo
      dest.addParRi()
    dest.addParRi()

proc trType(c: var Context; n: var Cursor) =
  ## Processes type declarations, adding hook pragmas for nominal types.
  ## For types with custom hooks (from sem), use those.
  ## For non-generic nominal types (objects/distincts), generate hooks via lifter.
  let info = n.info
  takeToken c.dest, n # (type
  var s = SymId(0)
  if n.kind == SymbolDef:
    s = n.symId
  c.dest.takeTree n # the symbol definition
  c.dest.takeTree n # exported
  let isGeneric = n.substructureKind == TypevarsU
  c.dest.takeTree n # typevars

  # Check if this is a non-generic nominal type that needs hooks generated
  var needsForgedHooks = false
  if not isGeneric and s != SymId(0):
    # Check if it's a nominal type (object, distinct)
    var checkBody = n
    skip checkBody # skip pragmas
    let bk = checkBody.typeKind
    needsForgedHooks = bk in {ObjectT, DistinctT, RefT}

  # Check if type has methods (for vtables)
  # Use the new c.classes table for method information
  var hasMethods = false
  var methodsToAdd: seq[(string, SymId)] = @[]
  if s != SymId(0) and s in c.classes:
    hasMethods = true
    # Convert MethodIndexEntry to (string, SymId) format for addMethodsDecl
    for entry in c.classes.getOrQuit(s).methods:
      let sig = pool.strings[entry.signature]
      methodsToAdd.add (sig, entry.fn)

  # pragmas:
  if s != SymId(0) and (c.hooks.hasKey(s) or hasMethods):
    # Type has custom hooks or methods from semantic analysis
    if n.kind == DotToken:
      c.dest.addParLe PragmasU, info
      inc n
    else:
      c.dest.takeToken n # existing pragma tag
      while n.kind != ParRi:
        c.dest.takeTree n # existing individual pragmas
      skipParRi n
    if c.hooks.hasKey(s):
      addHookDecls c.dest, c.hooks.getOrQuit(s)
    if hasMethods:
      addMethodsDecl c.dest, methodsToAdd
    c.dest.addParRi()
  elif needsForgedHooks:
    # Non-generic nominal type - generate hooks via lifter
    if n.kind == DotToken:
      c.dest.addParLe PragmasU, info
      inc n
    else:
      c.dest.takeToken n # existing pragma tag
      while n.kind != ParRi:
        c.dest.takeTree n # existing individual pragmas
      skipParRi n
    # Generate hooks via lifter - create Symbol buffer that stays alive:
    var buf = createTokenBuf(1)
    buf.addSymUse s, info
    let typeCursor = cursorAt(buf, 0)
    c.typeSymBufs.add buf
    # Collect methods for RTTI types (destroy/trace need to be methods)
    let isRtti = hasRtti(s)
    var rttiMethods = default seq[(string, SymId)]
    for op in low(AttachedOp)..high(AttachedOp):
      let hookProc = getHook(c.lifter[], op, typeCursor, info)
      if hookProc != SymId(0):
        c.dest.addParLe hookToTag(op), NoLineInfo
        c.dest.addSymUse hookProc, NoLineInfo
        c.dest.addParRi()
        # For RTTI types, destroy/trace are methods - add them with known signature
        if isRtti and op in {attachedDestroy, attachedTrace}:
          let key = case op
            of attachedDestroy: destroyMethodKey()
            of attachedTrace: traceMethodKey()
            else: ""
          rttiMethods.add((key, hookProc))
    # Emit methods pragma for RTTI types
    if rttiMethods.len > 0:
      addMethodsDecl c.dest, rttiMethods
    c.dest.addParRi() # close pragmas
  else:
    c.dest.takeTree n # pragmas
  c.dest.takeTree n # body
  c.dest.takeParRi n

proc tr(c: var Context; n: var Cursor; e: Expects) =
  case n.kind
  of Symbol:
    # Closures are now implemented
    let localInfo = c.typeCache.getLocalInfo(n.symId)
    if localInfo.crossedProc > 0 and localInfo.kind in {VarY, LetY, ParamY, ResultY} and
        IsClosure notin c.r.props:
      buildLocalErr c.dest, n.info, "cannot access local variable `" & asNimCode(n) & "` from another routine; mark the proc with `.closure`"
      skip n
      return

    if IsNoSideEffect in c.r.props:
      let res = tryLoadSym(n.symId)
      if res.status == LacksNothing:
        let local = asLocal(res.decl)
        if local.kind in {GvarY, TvarY}:
          buildLocalErr c.dest, n.info, "use of global/thread-local variable '" & asNimCode(n) & "' in .noSideEffect context"
          skip n
          return
    trLocation c, n, e
  of IntLit, UIntLit, FloatLit, CharLit, StringLit:
    if e.wantMutable:
      # Consider `fvar(returnsVar(someLet))`: We must not allow this.
      cannotPassToVar c.dest, n.info, n
      inc n
    else:
      takeToken c, n
  of UnknownToken, ParRi, EofToken, DotToken, Ident, SymbolDef:
    takeToken c, n
  of ParLe:
    case n.exprKind
    of CallKinds:
      var disallowDangerous = true
      trCall c, n, e, disallowDangerous
    of PragmaxX:
      trPragmaBlock c, n
    of DotX, DdotX, AtX, ArratX, TupatX, PatX:
      trLocation c, n, e
    of OconstrX, NewobjX:
      if e.wantMutable:
        cannotPassToVar c.dest, n.info, n
        skip n
      else:
        trObjConstr c, n, e
    of TupconstrX:
      if e.wantMutable:
        cannotPassToVar c.dest, n.info, n
        skip n
      else:
        trTupleConstr c, n, e
    of ExprX:
      trStmtListExpr c, n, e
    of DconvX:
      takeToken c, n
      takeTree c.dest, n # takes the type as it is
      tr c, n, e
      takeParRi c, n
    of BaseobjX:
      trSons c, n, WantT
    of ParX:
      trSons c, n, e
    of CopyX, WasmovedX, SinkhX, TraceX:
      trVarHook c, n
    of DupX, DestroyX:
      trSons c, n, WantT
    of HconvX, ConvX, CastX:
      if e.wantMutable:
        cannotPassToVar c.dest, n.info, n
        skip n
      else:
        trSons c, n, WantT
    of DerefX:
      if e.wantMutable:
        # allows ptr indirection: e.g. `inc x.id[]` for `id: ptr int`
        c.dest.addParLe(HaddrX, n.info)
        trSons c, n, WantT
        c.dest.addParRi()
      else:
        trSons c, n, WantT

    of KvX:
      if e.wantMutable:
        cannotPassToVar c.dest, n.info, n
        skip n
      else:
        trSons c, n, e

    else:
      case n.stmtKind
      of RetS:
        trReturn(c, n)
      of YldS:
        trYield(c, n)
      of AsgnS:
        trAsgn c, n
      of LocalDecls:
        trLocal c, n
      of ForS:
        trFor c, n
      of TryS:
        trTry c, n
      of ProcS, FuncS, MacroS, MethodS, ConverterS, IteratorS:
        trProcDecl c, n
      of ScopeS:
        c.typeCache.openScope()
        trSons c, n, WantT
        c.typeCache.closeScope()
      of TypeS:
        trType c, n
      else:
        if isDeclarative(n):
          takeTree c.dest, n
        else:
          trSons c, n, WantT

proc injectDerefs*(n: Cursor; hooks: sink Table[SymId, HooksPerType];
                   classes: sink Classes;
                   thisModuleSuffix: string; bits: int): TokenBuf =
  var c = Context(typeCache: createTypeCache(),
    r: CurrentRoutine(returnExpects: WantT, firstParam: NoSymId), dest: TokenBuf(),
    hooks: ensureMove(hooks),
    classes: ensureMove(classes),
    lifter: nil) # set below after hooks is moved
  # Pass address of hooks to lifter so it can look up hooks from current module
  c.lifter = createLiftingCtx(thisModuleSuffix, bits, addr c.hooks)
  c.typeCache.openScope()
  var n2 = n
  var n3 = n
  c.takeToken n2
  while n2.kind != ParRi:
    # clean up dots that sem might have introduced for moving inner generic instances:
    if n2.kind == DotToken: inc n2
    else: tr(c, n2, WantT)
  genMissingHooks c.lifter[], c.dest
  if c.r.dangerousLocations.len > 0:
    checkForDangerousLocations(c, n3)
  # Must close the `(stmts)` here **after** `checkForDangerousLocations`
  # because the latter may add error nodes.
  c.dest.addParRi()
  c.typeCache.closeScope()
  result = ensureMove(c.dest)
