/-
Copyright (c) 2021 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Daniel Selsam
-/
import Std.Data.RBMap
import Lean.Meta.SynthInstance
import Lean.Util.FindMVar
import Lean.Util.FindLevelMVar
import Lean.Util.CollectLevelParams
import Lean.Util.ReplaceLevel
import Lean.PrettyPrinter.Delaborator.Options
import Lean.PrettyPrinter.Delaborator.SubExpr
import Lean.Elab.Config

/-!
The top-down analyzer is an optional preprocessor to the delaborator that aims
to determine the minimal annotations necessary to ensure that the delaborated
expression can be re-elaborated correctly. Currently, the top-down analyzer
is neither sound nor complete: there may be edge-cases in which the expression
can still not be re-elaborated correctly, and it may also add many annotations
that are not strictly necessary.
-/

namespace Lean

open Lean.Meta
open Std (RBMap)

register_builtin_option pp.analyze : Bool := {
  defValue := false
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) determine annotations sufficient to ensure round-tripping"
}

register_builtin_option pp.analyze.checkInstances : Bool := {
  -- TODO: It would be great to make this default to `true`, but currently, `MessageData` does not
  -- include the `LocalInstances`, so this will be very over-aggressive in inserting instances
  -- that would otherwise be easy to synthesize. We may consider threading the instances in the future,
  -- or at least tracking a bool for whether the instances have been lost.
  defValue := false
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) confirm that instances can be re-synthesized"
}

register_builtin_option pp.analyze.typeAscriptions : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) add type ascriptions when deemed necessary"
}

register_builtin_option pp.analyze.trustSubst : Bool := {
  defValue := false
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) always 'pretend' applications that can delab to ▸ are 'regular'"
}

register_builtin_option pp.analyze.trustOfNat : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) always 'pretend' `OfNat.ofNat` applications can elab bottom-up"
}

register_builtin_option pp.analyze.trustOfScientific : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) always 'pretend' `OfScientific.ofScientific` applications can elab bottom-up"
}

register_builtin_option pp.analyze.trustCoe : Bool := {
  defValue := false
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) always assume a coercion can be correctly inserted"
}

-- TODO: this is an arbitrary special case of a more general principle.
register_builtin_option pp.analyze.trustSubtypeMk : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) assume the implicit arguments of Subtype.mk can be inferred"
}

register_builtin_option pp.analyze.trustId : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) always assume an implicit `fun x => x` can be inferred"
}

register_builtin_option pp.analyze.trustKnownFOType2TypeHOFuns : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) omit higher-order functions whose values seem to be knownType2Type"
}

register_builtin_option pp.analyze.omitMax : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) omit universe `max` annotations (these constraints can actually hurt)"
}

register_builtin_option pp.analyze.knowsType : Bool := {
  defValue := true
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) assume the type of the original expression is known"
}

register_builtin_option pp.analyze.explicitHoles : Bool := {
  defValue := false
  group    := "pp.analyze"
  descr    := "(pretty printer analyzer) use `_` for explicit arguments that can be inferred"
}

def getPPAnalyze                            (o : Options) : Bool := o.get pp.analyze.name pp.analyze.defValue
def getPPAnalyzeCheckInstances              (o : Options) : Bool := o.get pp.analyze.checkInstances.name pp.analyze.checkInstances.defValue
def getPPAnalyzeTypeAscriptions             (o : Options) : Bool := o.get pp.analyze.typeAscriptions.name pp.analyze.typeAscriptions.defValue
def getPPAnalyzeTrustSubst                  (o : Options) : Bool := o.get pp.analyze.trustSubst.name pp.analyze.trustSubst.defValue
def getPPAnalyzeTrustOfNat                  (o : Options) : Bool := o.get pp.analyze.trustOfNat.name pp.analyze.trustOfNat.defValue
def getPPAnalyzeTrustOfScientific           (o : Options) : Bool := o.get pp.analyze.trustOfScientific.name pp.analyze.trustOfScientific.defValue
def getPPAnalyzeTrustId                     (o : Options) : Bool := o.get pp.analyze.trustId.name pp.analyze.trustId.defValue
def getPPAnalyzeTrustCoe                    (o : Options) : Bool := o.get pp.analyze.trustCoe.name pp.analyze.trustCoe.defValue
def getPPAnalyzeTrustSubtypeMk              (o : Options) : Bool := o.get pp.analyze.trustSubtypeMk.name pp.analyze.trustSubtypeMk.defValue
def getPPAnalyzeTrustKnownFOType2TypeHOFuns (o : Options) : Bool := o.get pp.analyze.trustKnownFOType2TypeHOFuns.name pp.analyze.trustKnownFOType2TypeHOFuns.defValue
def getPPAnalyzeOmitMax                     (o : Options) : Bool := o.get pp.analyze.omitMax.name pp.analyze.omitMax.defValue
def getPPAnalyzeKnowsType                   (o : Options) : Bool := o.get pp.analyze.knowsType.name pp.analyze.knowsType.defValue
def getPPAnalyzeExplicitHoles               (o : Options) : Bool := o.get pp.analyze.explicitHoles.name pp.analyze.explicitHoles.defValue

def getPPAnalysisSkip            (o : Options) : Bool := o.get `pp.analysis.skip false
def getPPAnalysisHole            (o : Options) : Bool := o.get `pp.analysis.hole false
def getPPAnalysisNamedArg        (o : Options) : Bool := o.get `pp.analysis.namedArg false
def getPPAnalysisLetVarType      (o : Options) : Bool := o.get `pp.analysis.letVarType false
def getPPAnalysisNeedsType       (o : Options) : Bool := o.get `pp.analysis.needsType false
def getPPAnalysisBlockImplicit   (o : Options) : Bool := o.get `pp.analysis.blockImplicit false

namespace PrettyPrinter.Delaborator

def returnsPi (motive : Expr) : MetaM Bool := do
  lambdaTelescope motive fun xs b => return b.isForall

def isNonConstFun (motive : Expr) : MetaM Bool := do
  match motive with
  | Expr.lam name d b _ => isNonConstFun b
  | _ => return motive.hasLooseBVars

def isSimpleHOFun (motive : Expr) : MetaM Bool :=
  return not (← returnsPi motive) && not (← isNonConstFun motive)

def isType2Type (motive : Expr) : MetaM Bool := do
  match ← inferType motive with
  | Expr.forallE _ (Expr.sort ..) (Expr.sort ..) .. => return true
  | _ => return false

def isFOLike (motive : Expr) : MetaM Bool := do
  let f := motive.getAppFn
  return f.isFVar || f.isConst

def isIdLike (arg : Expr) : Bool :=
  -- TODO: allow `id` constant as well?
  match arg with
  | Expr.lam _ _ (Expr.bvar ..) .. => true
  | _ => false

def isCoe (e : Expr) : Bool :=
  -- TODO: `coeSort? Builtins doesn't seem to render them anyway
  -- TODO: should we delete this function, we want to eagerly expand all coercions
  e.isAppOfArity ``CoeT.coe 4
  || (e.isAppOf ``CoeFun.coe && e.getAppNumArgs >= 4)
  || e.isAppOfArity ``CoeSort.coe 4

def isStructureInstance (e : Expr) : MetaM Bool := do
  match e.isConstructorApp? (← getEnv) with
  | some s => return isStructure (← getEnv) s.induct
  | none   => return false

namespace TopDownAnalyze

partial def hasMVarAtCurrDepth (e : Expr) : MetaM Bool := do
  let mctx ← getMCtx
  return Option.isSome <| e.findMVar? fun mvarId =>
    match mctx.findDecl? mvarId with
    | some mdecl => mdecl.depth == mctx.depth
    | _ => false

partial def hasLevelMVarAtCurrDepth (e : Expr) : MetaM Bool := do
  let mctx ← getMCtx
  return Option.isSome <| e.findLevelMVar? fun mvarId =>
    mctx.findLevelDepth? mvarId == some mctx.depth

private def valUnknown (e : Expr) : MetaM Bool := do
  hasMVarAtCurrDepth (← instantiateMVars e)

private def typeUnknown (e : Expr) : MetaM Bool := do
  valUnknown (← inferType e)

def isHBinOp (e : Expr) : Bool := Id.run <| do
  -- TODO: instead of tracking these explicitly,
  -- consider a more general solution that checks for defaultInstances
  if e.getAppNumArgs != 6 then return false
  let f := e.getAppFn
  if !f.isConst then return false

  -- Note: we leave out `HPow.hPow because we expect its homogeneous
  -- version will change soon
  let ops := #[
    `HOr.hOr, `HXor.hXor, `HAnd.hAnd,
    `HAppend.hAppend, `HOrElse.hOrElse, `HAndThen.hAndThen,
    `HAdd.hAdd, `HSub.hSub, `HMul.hMul, `HDiv.hDiv, `HMod.hMod,
    `HShiftLeft.hShiftLeft, `HShiftRight]
  ops.any fun op => op == f.constName!

def replaceLPsWithVars (e : Expr) : MetaM Expr := do
  if !e.hasLevelParam then return e
  let lps := collectLevelParams {} e |>.params
  let mut replaceMap : Std.HashMap Name Level := {}
  for lp in lps do replaceMap := replaceMap.insert lp (← mkFreshLevelMVar)
  return e.replaceLevel fun
    | Level.param n .. => replaceMap.find! n
    | l => if !l.hasParam then some l else none

def isDefEqAssigning (t s : Expr) : MetaM Bool := do
  withReader (fun ctx => { ctx with config := { ctx.config with assignSyntheticOpaque := true }}) $
    Meta.isDefEq t s

def checkpointDefEq (t s : Expr) : MetaM Bool := do
  Meta.checkpointDefEq (mayPostpone := false) do
    isDefEqAssigning t s

def isHigherOrder (type : Expr) : MetaM Bool := do
  forallTelescopeReducing type fun xs b => return xs.size > 0 && b.isSort

def isFunLike (e : Expr) : MetaM Bool := do
  forallTelescopeReducing (← inferType e) fun xs b => return xs.size > 0

def isSubstLike (e : Expr) : Bool :=
  e.isAppOfArity `Eq.ndrec 6 || e.isAppOfArity `Eq.rec 6

def nameNotRoundtrippable (n : Name) : Bool :=
  n.hasMacroScopes || isPrivateName n || containsNum n
where
  containsNum
    | Name.str p .. => containsNum p
    | Name.num ..   => true
    | Name.anonymous => false

def mvarName (mvar : Expr) : MetaM Name :=
  return (← getMVarDecl mvar.mvarId!).userName

def containsBadMax : Level → Bool
  | Level.succ u ..   => containsBadMax u
  | Level.max u v ..  => (u.hasParam && v.hasParam) || containsBadMax u || containsBadMax v
  | Level.imax u v .. => (u.hasParam && v.hasParam) || containsBadMax u || containsBadMax v
  | _                 => false

open SubExpr

structure Context where
  knowsType   : Bool
  knowsLevel  : Bool -- only constants look at this
  inBottomUp  : Bool := false
  parentIsApp : Bool := false
  subExpr     : SubExpr
  deriving Inhabited

structure State where
  annotations : RBMap Pos Options compare := {}
  postponed   : Array (Expr × Expr) := #[] -- not currently used

abbrev AnalyzeM := ReaderT Context (StateRefT State MetaM)

instance (priority := low) : MonadReaderOf SubExpr AnalyzeM where
  read := Context.subExpr <$> read

instance (priority := low) : MonadWithReaderOf SubExpr AnalyzeM where
  withReader f x := fun ctx => x { ctx with subExpr := f ctx.subExpr }

def tryUnify (e₁ e₂ : Expr) : AnalyzeM Unit := do
  try
    let r ← isDefEqAssigning e₁ e₂
    if !r then modify fun s => { s with postponed := s.postponed.push (e₁, e₂) }
    pure ()
  catch ex =>
    modify fun s => { s with postponed := s.postponed.push (e₁, e₂) }

partial def inspectOutParams (arg mvar : Expr) : AnalyzeM Unit := do
  let argType  ← inferType arg -- HAdd α α α
  let mvarType ← inferType mvar
  let fType ← inferType argType.getAppFn -- Type → Type → outParam Type
  let mType ← inferType mvarType.getAppFn
  inspectAux fType mType 0 argType.getAppArgs mvarType.getAppArgs
where
  inspectAux (fType mType : Expr) (i : Nat) (args mvars : Array Expr) := do
    let fType ← whnf fType
    let mType ← whnf mType
    if not (i < args.size) then return ()
    match fType, mType with
    | Expr.forallE _ fd fb _, Expr.forallE _ md mb _ => do
      -- TODO: do I need to check (← okBottomUp? args[i] mvars[i] fuel).isSafe here?
      -- if so, I'll need to take a callback
      if isOutParam fd then
        tryUnify (args[i]) (mvars[i])
      inspectAux (fb.instantiate1 args[i]) (mb.instantiate1 mvars[i]) (i+1) args mvars
    | _, _ => return ()

partial def isTrivialBottomUp (e : Expr) : AnalyzeM Bool := do
  let opts ← getOptions
  return e.isFVar
         || e.isConst || e.isMVar || e.isNatLit || e.isStringLit || e.isSort
         || (getPPAnalyzeTrustOfNat opts && e.isAppOfArity `OfNat.ofNat 3)
         || (getPPAnalyzeTrustOfScientific opts && e.isAppOfArity `OfScientific.ofScientific 5)

partial def canBottomUp (e : Expr) (mvar? : Option Expr := none) (fuel : Nat := 10) : AnalyzeM Bool := do
  -- Here we check if `e` can be safely elaborated without its expected type.
  -- These are incomplete (and possibly unsound) heuristics.
  -- TODO: do I need to snapshot the state before calling this?
  match fuel with
  | 0 => return false
  | fuel + 1 =>
    if ← isTrivialBottomUp e then return true
    let f := e.getAppFn
    if !f.isConst && !f.isFVar then return false
    let args := e.getAppArgs
    let fType ← replaceLPsWithVars (← inferType e.getAppFn)
    let (mvars, bInfos, resultType) ← forallMetaBoundedTelescope fType e.getAppArgs.size
    for i in [:mvars.size] do
      if bInfos[i] == BinderInfo.instImplicit then
        inspectOutParams args[i] mvars[i]
      else if bInfos[i] == BinderInfo.default then
        if ← isTrivialBottomUp args[i] then tryUnify args[i] mvars[i]
        else if ← typeUnknown mvars[i] <&&> canBottomUp args[i] mvars[i] fuel then tryUnify args[i] mvars[i]
    if ← (pure (isHBinOp e) <&&> (valUnknown mvars[0] <||> valUnknown mvars[1])) then tryUnify mvars[0] mvars[1]
    if mvar?.isSome then tryUnify resultType (← inferType mvar?.get!)
    return !(← valUnknown resultType)

def withKnowing (knowsType knowsLevel : Bool) (x : AnalyzeM α) : AnalyzeM α := do
  withReader (fun ctx => { ctx with knowsType := knowsType, knowsLevel := knowsLevel }) x

builtin_initialize analyzeFailureId : InternalExceptionId ← registerInternalExceptionId `analyzeFailure

def checkKnowsType : AnalyzeM Unit := do
  if not (← read).knowsType then
    throw $ Exception.internal analyzeFailureId

def annotateBoolAt (n : Name) (pos : Pos) : AnalyzeM Unit := do
  let opts := (← get).annotations.findD pos {} |>.setBool n true
  trace[pp.analyze.annotate] "{pos} {n}"
  modify fun s => { s with annotations := s.annotations.insert pos opts }

def annotateBool (n : Name) : AnalyzeM Unit := do
  annotateBoolAt n (← getPos)

structure App.Context where
  f               : Expr
  fType           : Expr
  args            : Array Expr
  mvars           : Array Expr
  bInfos          : Array BinderInfo
  forceRegularApp : Bool

structure App.State where
  bottomUps       : Array Bool
  higherOrders    : Array Bool
  funBinders      : Array Bool
  provideds       : Array Bool
  namedArgs       : Array Name := #[]

abbrev AnalyzeAppM := ReaderT App.Context (StateT App.State AnalyzeM)

mutual

  partial def analyze (parentIsApp : Bool := false) : AnalyzeM Unit := do
    checkMaxHeartbeats "Delaborator.topDownAnalyze"
    trace[pp.analyze] "{(← read).knowsType}.{(← read).knowsLevel}"
    let e ← getExpr
    let opts ← getOptions
    if ← (pure !e.isAtomic) <&&> pure !(getPPProofs opts) <&&> (try Meta.isProof e catch ex => pure false) then
      if getPPProofsWithType opts then
        withType $ withKnowing true true $ analyze
      return ()
    else
      withReader (fun ctx => { ctx with parentIsApp := parentIsApp }) do
        match (← getExpr) with
        | Expr.app ..     => analyzeApp
        | Expr.forallE .. => analyzePi
        | Expr.lam ..     => analyzeLam
        | Expr.const ..   => analyzeConst
        | Expr.sort ..    => analyzeSort
        | Expr.proj ..    => analyzeProj
        | Expr.fvar ..    => analyzeFVar
        | Expr.mdata ..   => analyzeMData
        | Expr.letE ..    => analyzeLet
        | Expr.lit ..     => pure ()
        | Expr.mvar ..    => pure ()
        | Expr.bvar ..    => pure ()
  where
    analyzeApp := do
      let mut willKnowType := (← read).knowsType
      if !(← read).knowsType && !(← canBottomUp (← getExpr)) then
        annotateBool `pp.analysis.needsType
        withType $ withKnowing true false $ analyze
        willKnowType := true

      else if ← (pure !(← read).knowsType <||> pure (← read).inBottomUp) <&&> isStructureInstance (← getExpr) then
        withType do
          annotateBool `pp.structureInstanceTypes
          withKnowing true false $ analyze
        willKnowType := true

      withKnowing willKnowType true $ analyzeAppStaged (← getExpr).getAppFn (← getExpr).getAppArgs

    analyzeAppStaged (f : Expr) (args : Array Expr) : AnalyzeM Unit := do
      let fType ← replaceLPsWithVars (← inferType f)
      let (mvars, bInfos, resultType) ← forallMetaBoundedTelescope fType args.size
      let rest := args.extract mvars.size args.size
      let args := args.shrink mvars.size

      -- Unify with the expected type
      if (← read).knowsType then tryUnify (← inferType (mkAppN f args)) resultType

      let forceRegularApp : Bool :=
        (getPPAnalyzeTrustSubst (← getOptions) && isSubstLike (← getExpr))
        || (getPPAnalyzeTrustCoe (← getOptions) && isCoe (← getExpr))
        || (getPPAnalyzeTrustSubtypeMk (← getOptions) && (← getExpr).isAppOfArity `Subtype.mk 4)

      analyzeAppStagedCore { f, fType, args, mvars, bInfos, forceRegularApp } |>.run' {
        bottomUps    := mkArray args.size false,
        higherOrders := mkArray args.size false,
        provideds    := mkArray args.size false,
        funBinders   := mkArray args.size false
      }

      if not rest.isEmpty then
        -- Note: this shouldn't happen for type-correct terms
        if !args.isEmpty then
          analyzeAppStaged (mkAppN f args) rest

    maybeAddBlockImplicit : AnalyzeM Unit := do
      -- See `MonadLift.noConfusion for an example where this is necessary.
      if !(← read).parentIsApp then
        let type ← inferType (← getExpr)
        if type.isForall && type.bindingInfo! == BinderInfo.implicit then
          annotateBool `pp.analysis.blockImplicit

    analyzeConst : AnalyzeM Unit := do
      let Expr.const n ls .. ← getExpr | unreachable!
      if !(← read).knowsLevel && !ls.isEmpty then
        -- TODO: this is a very crude heuristic, motivated by https://github.com/leanprover/lean4/issues/590
        unless getPPAnalyzeOmitMax (← getOptions) && ls.any containsBadMax do
        annotateBool `pp.universes
      maybeAddBlockImplicit

    analyzePi : AnalyzeM Unit := do
      withBindingDomain $ withKnowing true false analyze
      withBindingBody Name.anonymous analyze

    analyzeLam : AnalyzeM Unit := do
      if !(← read).knowsType then annotateBool `pp.funBinderTypes
      withBindingDomain $ withKnowing true false analyze
      withBindingBody Name.anonymous analyze

    analyzeLet : AnalyzeM Unit := do
      let Expr.letE n t v body .. ← getExpr | unreachable!
      if !(← canBottomUp v) then
        annotateBool `pp.analysis.letVarType
        withLetVarType $ withKnowing true false analyze
        withLetValue $ withKnowing true true analyze
      else
        withReader (fun ctx => { ctx with inBottomUp := true }) do
          withLetValue $ withKnowing true true analyze

      withLetBody analyze

    analyzeSort  : AnalyzeM Unit := pure ()
    analyzeProj  : AnalyzeM Unit := withProj analyze
    analyzeFVar  : AnalyzeM Unit := maybeAddBlockImplicit
    analyzeMData : AnalyzeM Unit := withMDataExpr analyze

  partial def analyzeAppStagedCore : AnalyzeAppM Unit := do
    collectBottomUps
    checkOutParams
    collectHigherOrders
    hBinOpHeuristic
    collectTrivialBottomUps
    discard <| processPostponed (mayPostpone := true)
    applyFunBinderHeuristic
    analyzeFn
    for i in [:(← read).args.size] do analyzeArg i
    maybeSetExplicit

  where
    collectBottomUps := do
      let { args, mvars, bInfos, ..} ← read
      for target in [fun _ => none, fun i => some mvars[i]] do
        for i in [:args.size] do
          if bInfos[i] == BinderInfo.default then
            if ← typeUnknown mvars[i] <&&> canBottomUp args[i] (target i) then
              tryUnify args[i] mvars[i]
              modify fun s => { s with bottomUps := s.bottomUps.set! i true }

    checkOutParams := do
      let { args, mvars, bInfos, ..} ← read
      for i in [:args.size] do
        if bInfos[i] == BinderInfo.instImplicit then inspectOutParams args[i] mvars[i]

    collectHigherOrders := do
      let { args, mvars, bInfos, ..} ← read
      for i in [:args.size] do
        if not (bInfos[i] == BinderInfo.implicit || bInfos[i] == BinderInfo.strictImplicit) then continue
        if not (← isHigherOrder (← inferType args[i])) then continue
        if getPPAnalyzeTrustId (← getOptions) && isIdLike args[i] then continue

        if getPPAnalyzeTrustKnownFOType2TypeHOFuns (← getOptions) && not (← valUnknown mvars[i])
          && (← isType2Type (args[i])) && (← isFOLike (args[i])) then continue

        tryUnify args[i] mvars[i]
        modify fun s => { s with higherOrders := s.higherOrders.set! i true }

    hBinOpHeuristic := do
      let { args, mvars, bInfos, ..} ← read
      if ← (pure (isHBinOp (← getExpr)) <&&> (valUnknown mvars[0] <||> valUnknown mvars[1])) then
        tryUnify mvars[0] mvars[1]

    collectTrivialBottomUps := do
      -- motivation: prevent levels from printing in
      -- Boo.mk : {α : Type u_1} → {β : Type u_2} → α → β → Boo.{u_1, u_2} α β
      let { args, mvars, bInfos, ..} ← read
      for i in [:args.size] do
        if bInfos[i] == BinderInfo.default then
          if ← valUnknown mvars[i] <&&> isTrivialBottomUp args[i] then
            tryUnify args[i] mvars[i]
            modify fun s => { s with bottomUps := s.bottomUps.set! i true }

    applyFunBinderHeuristic := do
      let { f, args, mvars, bInfos, .. } ← read

      let rec core (argIdx : Nat) (mvarType : Expr) : AnalyzeAppM Bool := do
        match ← getExpr, mvarType with
        | Expr.lam .., Expr.forallE n t b .. =>
          let mut annotated := false
          for i in [:argIdx] do
            if ← pure (bInfos[i] == BinderInfo.implicit) <&&> valUnknown mvars[i] <&&> withNewMCtxDepth (checkpointDefEq t mvars[i]) then
              annotateBool `pp.funBinderTypes
              tryUnify args[i] mvars[i]
              -- Note: currently we always analyze the lambda binding domains in `analyzeLam`
              -- (so we don't need to analyze it again here)
              annotated := true
              break
          let annotatedBody ← withBindingBody Name.anonymous (core argIdx b)
          return annotated || annotatedBody

        | _, _ => return false

      for i in [:args.size] do
        if bInfos[i] == BinderInfo.default then
          let b ← withNaryArg i (core i (← inferType mvars[i]))
          if b then modify fun s => { s with funBinders := s.funBinders.set! i true }

    analyzeFn := do
      -- Now, if this is the first staging, analyze the n-ary function without expected type
      let {f, fType, forceRegularApp ..} ← read
      if !f.isApp then withKnowing false (forceRegularApp || !(← hasLevelMVarAtCurrDepth (← instantiateMVars fType))) $ withNaryFn (analyze (parentIsApp := true))

    annotateNamedArg (n : Name) : AnalyzeAppM Unit := do
      annotateBool `pp.analysis.namedArg
      modify fun s => { s with namedArgs := s.namedArgs.push n }

    analyzeArg (i : Nat) := do
      let { f, args, mvars, bInfos, forceRegularApp ..} ← read
      let { bottomUps, higherOrders, funBinders, ..} ← get
      let arg := args[i]
      let argType ← inferType arg

      let processNaturalImplicit : AnalyzeAppM Unit := do
        if (← valUnknown mvars[i] <||> pure higherOrders[i]) && !forceRegularApp then
          annotateNamedArg (← mvarName mvars[i])
          modify fun s => { s with provideds := s.provideds.set! i true }
        else
          annotateBool `pp.analysis.skip

      withNaryArg (f.getAppNumArgs + i) do
        withTheReader Context (fun ctx => { ctx with inBottomUp := ctx.inBottomUp || bottomUps[i] }) do

          match bInfos[i] with
          | BinderInfo.default =>
            if ← pure (getPPAnalyzeExplicitHoles (← getOptions)) <&&> pure !(← valUnknown mvars[i]) <&&> pure !(← readThe Context).inBottomUp <&&> pure !(← isFunLike arg) <&&> pure !funBinders[i] <&&> checkpointDefEq mvars[i] arg then
              annotateBool `pp.analysis.hole
            else
              modify fun s => { s with provideds := s.provideds.set! i true }

          | BinderInfo.implicit => processNaturalImplicit
          | BinderInfo.strictImplicit => processNaturalImplicit

          | BinderInfo.instImplicit =>
            -- Note: apparently checking valUnknown here is not sound, because the elaborator
            -- will not happily assign instImplicits that it cannot synthesize
            let mut provided := true
            if !getPPInstances (← getOptions) then
              annotateBool `pp.analysis.skip
              provided := false
            else if getPPAnalyzeCheckInstances (← getOptions) then
              let instResult ← try trySynthInstance argType catch _ => pure LOption.undef
              match instResult with
              | LOption.some inst =>
                if ← checkpointDefEq inst arg then annotateBool `pp.analysis.skip; provided := false
                else annotateNamedArg (← mvarName mvars[i])
              | _                 => annotateNamedArg (← mvarName mvars[i])
            else annotateBool `pp.analysis.skip; provided := false
            modify fun s => { s with provideds := s.provideds.set! i provided }
          | BinderInfo.auxDecl => pure ()
          if (← get).provideds[i] then withKnowing (not (← typeUnknown mvars[i])) true analyze
          tryUnify mvars[i] args[i]

    maybeSetExplicit := do
      let { f, args, mvars, bInfos, forceRegularApp, ..} ← read
      if (← get).namedArgs.any nameNotRoundtrippable then
        annotateBool `pp.explicit
        for i in [:args.size] do
          if !(← get).provideds[i] then
            withNaryArg (f.getAppNumArgs + i) do annotateBool `pp.analysis.hole
          if bInfos[i] == BinderInfo.instImplicit && getPPInstanceTypes (← getOptions) then
            withType (withKnowing true false analyze)

end

end TopDownAnalyze

open TopDownAnalyze SubExpr

def topDownAnalyze (e : Expr) : MetaM OptionsPerPos := do
  let s₀ ← get
  traceCtx `pp.analyze do
    withReader (fun ctx => { ctx with config := Elab.Term.setElabConfig ctx.config }) do
      let ϕ : AnalyzeM OptionsPerPos := do withNewMCtxDepth analyze; pure (← get).annotations
      try
        let knowsType := getPPAnalyzeKnowsType (← getOptions)
        ϕ { knowsType := knowsType, knowsLevel := knowsType, subExpr := mkRoot e }
          |>.run' { : TopDownAnalyze.State }
      catch ex =>
        trace[pp.analyze.error] "failed"
        pure {}
      finally set s₀

builtin_initialize
  registerTraceClass `pp.analyze
  registerTraceClass `pp.analyze.annotate
  registerTraceClass `pp.analyze.tryUnify
  registerTraceClass `pp.analyze.error

end Lean.PrettyPrinter.Delaborator


import LeanInk.Annotation.Basic
import LeanInk.Logger
import LeanInk.Analysis.DataTypes
import LeanInk.Annotation.Util
import LeanInk.FileHelper

import Lean.Data.Json
import Lean.Data.Json.FromToJson
import Lean.Data.Lsp

namespace LeanInk.Annotation.Alectryon

open Lean
open LeanInk.Analysis

structure TypeInfo where
  _type : String := "typeinfo"
  name : String
  type : String
  deriving ToJson

structure Token where
  _type : String := "token"
  raw : String
  typeinfo : Option TypeInfo := Option.none
  link : Option String := Option.none
  docstring : Option String := Option.none
  semanticType : Option String :=  Option.none
  deriving ToJson

/--
  Support type for experimental --experimental-type-tokens feature
  If flag not set please only use the string case.
-/
inductive Contents where
  | string (value : String)
  | experimentalTokens (value : Array Token)

instance : ToJson Contents where
  toJson
    | Contents.string v => toJson v
    | Contents.experimentalTokens v => toJson v

structure Hypothesis where
  _type : String := "hypothesis"
  names : List String
  body : String
  type : String
  deriving ToJson

structure Goal where
  _type : String := "goal"
  name : String
  conclusion : String
  hypotheses : Array Hypothesis
  deriving ToJson

structure Message where
  _type : String := "message"
  contents : String
  deriving ToJson

structure Sentence where
  _type : String := "sentence"
  contents : Contents
  messages : Array Message
  goals : Array Goal
  deriving ToJson

structure Text where
  _type : String := "text"
  contents : Contents
  deriving ToJson

/--
We need a custom ToJson implementation for Alectryons fragments.

For example we have following fragment:
```
Fragment.text { contents := "Test" }
```

We want to serialize this to:
```
[{"contents": "Test", "_type": "text"}]
```

instead of:
```
[{"text": {"contents": "Test", "_type": "text"}}]
```

This is because of the way Alectryon decodes the json files. It uses the _type field to
determine the namedTuple type with Alectryon.
-/
inductive Fragment where
  | text (value : Text)
  | sentence (value : Sentence)

instance : ToJson Fragment where
  toJson
    | Fragment.text v => toJson v
    | Fragment.sentence v => toJson v

/-
  Token Generation
-/

def genTypeInfo? (getContents : String.Pos -> String.Pos -> Option String) (token : Analysis.TypeTokenInfo) : AnalysisM (Option TypeInfo) := do
  match token.type with
  | some type => do
    let headPos := Positional.headPos token
    let tailPos := Positional.tailPos token
    match getContents headPos tailPos with
    | none => pure none
    | "" => pure none
    | some x => return some { name := x, type := type }
  | none => pure none

def genSemanticTokenValue : Option SemanticTokenInfo -> AnalysisM (Option String)
  | none => pure none
  | some info =>
    match info.semanticType with
    | SemanticTokenType.property => pure (some "Name.Attribute")
    | SemanticTokenType.keyword => pure (some "Keyword")
    | SemanticTokenType.variable => pure (some "Name.Variable")
    | default => pure none

def genToken (token : Compound Analysis.Token) (contents : Option String) (getContents : String.Pos -> String.Pos -> Option String) : AnalysisM (Option Token) := do
  match contents with
  | none => return none
  | "" => return none
  | some contents => do
    let typeTokens := token.getFragments.filterMap (λ x => x.toTypeTokenInfo?)
    let semanticTokens := token.getFragments.filterMap (λ x => x.toSemanticTokenInfo?)
    let semanticToken := Positional.smallest? semanticTokens
    let semanticTokenType ← genSemanticTokenValue semanticToken
    match (Positional.smallest? typeTokens) with
    | none => do
      return some { raw := contents, semanticType := semanticTokenType }
    | some token => do
      return some { raw := contents, typeinfo := ← genTypeInfo? getContents token, link := none, docstring := token.docString, semanticType := semanticTokenType }

def extractContents (offset : String.Pos) (contents : String) (head tail: String.Pos) : Option String :=
  if head >= tail then
    none
  else
    contents.extract (head - offset) (tail - offset)

def minPos (x y : String.Pos) := if x < y then x else y
def maxPos (x y : String.Pos) := if x > y then x else y

partial def genTokens (contents : String) (head : String.Pos) (offset : String.Pos) (l : List Token)  (compounds : List (Compound Analysis.Token)) : AnalysisM (List Token) := do
  let textTail := ⟨contents.utf8ByteSize⟩ + offset
  let mut head : String.Pos := head
  let mut tokens : List Token := []
  for x in compounds do
    let extract := extractContents offset contents
    let tail := x.tailPos.getD textTail
    if x.headPos <= head then
      let text := extract head tail
      head := tail
      logInfo s!"Text-B1: {text}"
      match (← genToken x text extract) with
      | none => logInfo s!"Empty 1 {text} {x.headPos} {tail}"
      | some fragment => tokens := fragment::tokens
    else
      let text := extract head x.headPos
      head := x.headPos
      logInfo s!"Text-B2: {text}"
      match text with
      | none => logInfo s!"Empty 1 {text} {x.headPos} {tail}"
      | some text => tokens := { raw := text }::tokens
  match extractContents offset contents head (⟨contents.utf8ByteSize⟩ + offset) with
  | none => return tokens.reverse
  | some x => return ({ raw := x }::tokens).reverse

/-
  Fragment Generation
-/

def genHypothesis (hypothesis : Analysis.Hypothesis) : Hypothesis := {
  names := hypothesis.names
  body := hypothesis.body
  type := hypothesis.type
}

def genGoal (goal : Analysis.Goal) : Goal := {
  name := goal.name
  conclusion := goal.conclusion
  hypotheses := (goal.hypotheses.map genHypothesis).toArray
}

def genGoals (beforeNode: Bool) (tactic : Analysis.Tactic) : List Goal :=
  if beforeNode then
    tactic.goalsBefore.map (λ g => genGoal g)
  else
    tactic.goalsAfter.map (λ g => genGoal g)

def genMessages (message : Analysis.Message) : Message := { contents := message.msg }

def genFragment (annotation : Annotation) (globalTailPos : String.Pos) (contents : String) : AnalysisM Alectryon.Fragment := do
  let config ← read
  if annotation.sentence.fragments.isEmpty then
    if config.experimentalTypeInfo ∨ config.experimentalDocString then
      let headPos := annotation.sentence.headPos
      let tokens ← genTokens contents headPos headPos [] annotation.tokens
      return Fragment.text { contents := Contents.experimentalTokens tokens.toArray }
    else
      return Fragment.text { contents := Contents.string contents }
  else
    let tactics : List Analysis.Tactic := annotation.sentence.getFragments.filterMap (λ f => f.asTactic?)
    let messages : List Analysis.Message := annotation.sentence.getFragments.filterMap (λ f => f.asMessage?)
    let mut goals : List Goal := []
    if let (some tactic) := Positional.smallest? tactics then
      let useBefore : Bool := tactic.tailPos > globalTailPos
      goals := genGoals useBefore tactic
    let mut fragmentContents : Contents := Contents.string contents
    if config.experimentalTypeInfo ∨ config.experimentalDocString then
      let headPos := annotation.sentence.headPos
      let tokens ← genTokens contents headPos headPos [] annotation.tokens
      fragmentContents := Contents.experimentalTokens tokens.toArray
    return Fragment.sentence {
      contents := fragmentContents
      goals := goals.toArray
      messages := (messages.map genMessages).toArray
    }

/-
Expects a list of sorted CompoundFragments (sorted by headPos).
Generates AlectryonFragments for the given CompoundFragments and input file content.
-/
def annotateFileWithCompounds (l : List Alectryon.Fragment) (contents : String) : List Annotation -> AnalysisM (List Fragment)
| [] => pure l
| x::[] => do
  let fragment ← genFragment x ⟨contents.utf8ByteSize⟩ (contents.extract x.sentence.headPos ⟨contents.utf8ByteSize⟩)
  return l.append [fragment]
| x::y::ys => do
  let fragment ← genFragment x y.sentence.headPos (contents.extract x.sentence.headPos (y.sentence.headPos))
  return (← annotateFileWithCompounds (l.append [fragment]) contents (y::ys))

def genOutput (annotation : List Annotation) : AnalysisM UInt32 := do
  let config := (← read)
  let fragments ← annotateFileWithCompounds [] config.inputFileContents annotation
  let rawContents ← generateOutput fragments.toArray
  createOutputFile (← IO.currentDir) config.inputFileName rawContents
  return 0


import .hp hp.writeup basic
universes u

open widget
open tactic.unsafe
open tactic

namespace hp

meta def rc (π α) := interaction_component hp_state π α

meta instance : has_should_update hp_state :=
{su := λ rs₁ rs₂, (rs₁.b ≠ rs₂.b)}

variables {π α : Type} [has_should_update π]

-- meta instance tc_coe_rc: has_coe (tc π α) (rc π α) :=
-- ⟨λ c, interaction_component.bind_props (λ p, do ts ← hp_state.ts <$> get, pure (ts,p)) $ interaction_component.of_component c⟩

meta def rc.to_component : rc π α → component (hp_state × π) α
| r := interaction_component.to_component r

meta instance rc_fun : has_coe_to_fun (rc π α) :=
⟨ λ c, π → hp (html α)
, λ c p, do rs ← get, pure $ html.of_component (rs, p) c.to_component
⟩

meta def rc.stateless {π α} [has_should_update π] (view : π → hp (list (html α))): rc π α :=
interaction_component.stateless (λ p, view p)

/-- An rc for rendering as a secondary tactic type.
The main example being subtasks. -/
meta def inner_component := html (hp_state)

meta inductive rc.action : Type
| new_state (rs : hp_state)
| interactive_inner_tactic (rc : inner_component)

end hp


/-
Copyright (c) 2021 Henrik Böving. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Henrik Böving
-/
import DocGen4.Process
import DocGen4.Output.ToHtmlFormat

namespace DocGen4.Output

open scoped DocGen4.Jsx
open Lean System Widget Elab Process

def basePath := FilePath.mk "." / "build" / "doc"
def srcBasePath := basePath / "src"
def declarationsBasePath := basePath / "declarations"

/--
The context used in the `BaseHtmlM` monad for HTML templating.
-/
structure SiteBaseContext where

  /--
  The module hierarchy as a tree structure.
  -/
  hierarchy : Hierarchy
  /--
  How far away we are from the page root, used for relative links to the root.
  -/
  depthToRoot: Nat
  /--
  The name of the current module if there is one, there exist a few
  pages that don't have a module name.
  -/
  currentName : Option Name
  /--
  The Github URL of the project that we are building docs for.
  -/
  projectGithubUrl : String
  /--
  The commit of the project that we are building docs for.
  -/
  projectCommit : String

/--
The context used in the `HtmlM` monad for HTML templating.
-/
structure SiteContext where
  /--
  The full analysis result from the Process module.
  -/
  result : AnalyzerResult
  /--
  A function to link declaration names to their source URLs, usually Github ones.
  -/
  sourceLinker : Name → Option DeclarationRange → String
  /--
  Whether LeanInk is enabled
  -/
  leanInkEnabled : Bool

def setCurrentName (name : Name) (ctx : SiteBaseContext) := {ctx with currentName := some name}

abbrev BaseHtmlT := ReaderT SiteBaseContext
abbrev BaseHtmlM := BaseHtmlT Id

abbrev HtmlT (m) := ReaderT SiteContext (BaseHtmlT m)
abbrev HtmlM := HtmlT Id

def HtmlT.run (x : HtmlT m α) (ctx : SiteContext) (baseCtx : SiteBaseContext) : m α :=
  ReaderT.run x ctx |>.run baseCtx

def HtmlM.run (x : HtmlM α) (ctx : SiteContext) (baseCtx : SiteBaseContext) : α :=
  ReaderT.run x ctx |>.run baseCtx |>.run

instance [Monad m] : MonadLift HtmlM (HtmlT m) where
  monadLift x := do return x.run (← readThe SiteContext) (← readThe SiteBaseContext)

instance [Monad m] : MonadLift BaseHtmlM (BaseHtmlT m) where
  monadLift x := do return x.run (← readThe SiteBaseContext)

/--
Obtains the root URL as a relative one to the current depth.
-/
def getRoot : BaseHtmlM String := do
  let rec go: Nat -> String
  | 0 => "./"
  | Nat.succ n' => "../" ++ go n'
  let d <- SiteBaseContext.depthToRoot <$> read
  return (go d)

def getHierarchy : BaseHtmlM Hierarchy := do return (← read).hierarchy
def getCurrentName : BaseHtmlM (Option Name) := do return (← read).currentName
def getResult : HtmlM AnalyzerResult := do return (← read).result
def getSourceUrl (module : Name) (range : Option DeclarationRange): HtmlM String := do return (← read).sourceLinker module range
def leanInkEnabled? : HtmlM Bool := do return (← read).leanInkEnabled
def getProjectGithubUrl : BaseHtmlM String := do return (← read).projectGithubUrl
def getProjectCommit : BaseHtmlM String := do return (← read).projectCommit

/--
If a template is meant to be extended because it for example only provides the
header but no real content this is the way to fill the template with content.
This is untyped so HtmlM and BaseHtmlM can be mixed.
-/
def templateExtends {α β} {m} [Bind m] (base : α → m β) (new : m α) : m β :=
  new >>= base

def templateLiftExtends {α β} {m n} [Bind m] [MonadLift n m] (base : α → n β) (new : m α) : m β :=
  new >>= (monadLift ∘ base)
/--
Returns the doc-gen4 link to a module name.
-/
def moduleNameToLink (n : Name) : BaseHtmlM String := do
  let parts := n.components.map Name.toString
  return (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html"

/--
Returns the HTML doc-gen4 link to a module name.
-/
def moduleToHtmlLink (module : Name) : BaseHtmlM Html := do
  return {module.toString}

/--
Returns the LeanInk link to a module name.
-/
def moduleNameToInkLink (n : Name) : BaseHtmlM String := do
  let parts := "src" :: n.components.map Name.toString
  return (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html"

/--
Returns the path to the HTML file that contains information about a module.
-/
def moduleNameToFile (basePath : FilePath) (n : Name) : FilePath :=
  let parts := n.components.map Name.toString
  FilePath.withExtension (basePath / parts.foldl (· / ·) (FilePath.mk ".")) "html"

/--
Returns the directory of the HTML file that contains information about a module.
-/
def moduleNameToDirectory (basePath : FilePath) (n : Name) : FilePath :=
  let parts := n.components.dropLast.map Name.toString
  basePath / parts.foldl (· / ·) (FilePath.mk ".")

section Static
/-!
The following section contains all the statically included files that
are used in documentation generation, notably JS and CSS ones.
-/
  def styleCss : String := include_str "../../static/style.css"
  def declarationDataCenterJs : String := include_str "../../static/declaration-data.js"
  def navJs : String := include_str "../../static/nav.js"
  def howAboutJs : String := include_str "../../static/how-about.js"
  def searchJs : String := include_str "../../static/search.js"
  def instancesJs : String := include_str "../../static/instances.js"
  def importedByJs : String := include_str "../../static/importedBy.js"
  def findJs : String := include_str "../../static/find/find.js"
  def mathjaxConfigJs : String := include_str "../../static/mathjax-config.js"

  def alectryonCss : String := include_str "../../static/alectryon/alectryon.css"
  def alectryonJs : String := include_str "../../static/alectryon/alectryon.js"
  def docUtilsCss : String  := include_str "../../static/alectryon/docutils_basic.css"
  def pygmentsCss : String  := include_str "../../static/alectryon/pygments.css"
end Static

/--
Returns the doc-gen4 link to a declaration name.
-/
def declNameToLink (name : Name) : HtmlM String := do
  let res ← getResult
  let module := res.moduleNames[res.name2ModIdx.find! name |>.toNat]!
  return (← moduleNameToLink module) ++ "#" ++ name.toString

/--
Returns the HTML doc-gen4 link to a declaration name.
-/
def declNameToHtmlLink (name : Name) : HtmlM Html := do
  return {name.toString}

/--
Returns the LeanInk link to a declaration name.
-/
def declNameToInkLink (name : Name) : HtmlM String := do
  let res ← getResult
  let module := res.moduleNames[res.name2ModIdx.find! name |>.toNat]!
  return (← moduleNameToInkLink module) ++ "#" ++ name.toString

/--
Returns a name splitted into parts.
Together with "break_within" CSS class this helps browser to break a name
nicely.
-/
def breakWithin (name: String) : (Array Html) :=
  name.splitOn "."
    |> .map (fun (s: String) => {s})
    |> .intersperse "."
    |> List.toArray

/--
Returns the HTML doc-gen4 link to a declaration name with "break_within"
set as class.
-/
def declNameToHtmlBreakWithinLink (name : Name) : HtmlM Html := do
  return
      [breakWithin name.toString]

/--
In Lean syntax declarations the following pattern is quite common:
```
syntax term " + " term : term
```
that is, we place spaces around the operator in the middle. When the
`InfoTree` framework provides us with information about what source token
corresponds to which identifier it will thus say that `" + "` corresponds to
`HAdd.hadd`. This is however not the way we want this to be linked, in the HTML
only `+` should be linked, taking care of this is what this function is
responsible for.
-/
def splitWhitespaces (s : String) : (String × String × String) := Id.run do
  let front := "".pushn ' ' <| s.offsetOfPos (s.find (!Char.isWhitespace ·))
  let mut s := s.trimLeft
  let back := "".pushn ' ' (s.length - s.offsetOfPos (s.find Char.isWhitespace))
  s := s.trimRight
  (front, s, back)

/--
Turns a `CodeWithInfos` object, that is basically a Lean syntax tree with
information about what the identifiers mean, into an HTML object that links
to as much information as possible.
-/
partial def infoFormatToHtml (i : CodeWithInfos) : HtmlM (Array Html) := do
  match i with
  | .text t => return #[Html.escape t]
  | .append tt => tt.foldlM (fun acc t => do return acc ++ (← infoFormatToHtml t)) #[]
  | .tag a t =>
    match a.info.val.info with
    | Info.ofTermInfo i =>
      let cleanExpr :=  i.expr.consumeMData
      match cleanExpr with
      | .const name _ =>
        -- TODO: this is some very primitive blacklisting but real Blacklisting needs MetaM
        -- find a better solution
        if (← getResult).name2ModIdx.contains name then
          match t with
          | .text t =>
            let (front, t, back) := splitWhitespaces <| Html.escape t
            let elem := {t}
            return #[Html.text front, elem, Html.text back]
          | _ =>
            return #[[← infoFormatToHtml t]]
        else
         return #[[← infoFormatToHtml t]]
      | .sort _ =>
        match t with
        | .text t =>
          let mut sortPrefix :: rest := t.splitOn " " | unreachable!
          let sortLink := {sortPrefix}
          if rest != [] then
            rest := " " :: rest
          return #[sortLink, Html.text <| String.join rest]
        | _ =>
          return #[[← infoFormatToHtml t]]
      | _ =>
         return #[[← infoFormatToHtml t]]
    | _ => return #[[← infoFormatToHtml t]]

def baseHtmlHeadDeclarations : BaseHtmlM (Array Html) := do
  return #[
    <meta charset="UTF-8"/>,
    <meta name="viewport" content="width=device-width, initial-scale=1"/>,
    <link rel="stylesheet" href={s!"{← getRoot}style.css"}/>,
    <link rel="stylesheet" href={s!"{← getRoot}src/pygments.css"}/>,
    <link rel="shortcut icon" href={s!"{← getRoot}favicon.ico"}/>,
    <link rel="prefetch" href={s!"{← getRoot}/declarations/declaration-data.bmp"} as="image"/>
  ]

end DocGen4.Output