// Which forms are legal WHERE.
//
// The policy used to be in two halves that never met. A `kind_of` / `in_group`
// table said which of ten flat shapes each parsing context admitted; a
// `lower_expr` in the same file said, with a sentence each, which of the
// expression grammar's forms a slot cannot hold at all. The first answered
// single tokens, the second answered everything longer, and the two disagreed
// about the same input often enough that "which path did this take" was a
// question a reader had to ask.
//
// One question now, asked of the expression itself: may this form stand HERE.
// It lives beside the AST rather than in the parser because it is a fact about
// the language and not about reading it — the generator asks it, a checker
// could, and a second front end would otherwise invent its own answer.
//
// The sentences are the contract. `tscript/parse_test.mbt` snapshots them, and
// a diff there is a bug rather than a rewording: the refusal is what an author
// reads, and it names what to write instead.

///|
/// A position an expression can be written in.
///
/// One case per slot the view language has, plus the block body. They are not
/// a hierarchy and they are not orderable: `@each` admits a dynamic and a
/// field and nothing else, an `@on` argument admits an event read that no
/// other position may hold, and `provide` admits exactly the two shapes that
/// can be ADDRESSED because a provide doubles as a resume target.
pub(all) enum Position {
  /// A condition: `@show`, `@hide`, `@when`, `@if.`, a `pred` body.
  PosCondition
  /// Text: `@text`, an interpolated attribute value.
  PosText
  /// A render target: ``.
  PosComponent
  /// An iteration source: `@each`, ``.
  PosSequence
  /// A `provide` expression. Read as `*name` AND resumed at, so it must be
  /// addressable — a field or a seq access, nothing else.
  PosProvide
  /// A field-shaped slot: a `lookup` default, a bind target.
  PosField
  /// An argument of an `@on` handler. The one position an `e.` may
  /// stand in.
  PosEventArg
  /// The HANDLER of an `@on`: the name of the thing a raised event runs.
  PosEvent
  /// The handler of a render-time directive — `@when`, `@enrich-with`,
  /// `@loop-with`. A name here is answered by the render stack rather than
  /// dispatched, which is the whole of what the retired `HandlerNamespace`
  /// said.
  PosTrigger
  /// A macro attribute: whatever a slot can hold, because the callee's own
  /// position decides what it means.
  PosMacroAttr
  /// A declaration body. The whole grammar — this is the position the
  /// expression language was written for.
  PosBody
} derive(Debug, Eq)

///|
/// What a position answers about a form.
///
/// Three answers rather than two, because "no" comes in two kinds and they are
/// reported differently. A form that is simply not in this position's set is
/// `NotHere`: the caller says its own bad-value sentence, naming the slot. A
/// form the slot LANGUAGE cannot hold at any depth — a nested place, an `if`,
/// a `&position` — is `Refused`, and carries the sentence that names what to
/// write instead, because that one is about the expression rather than about
/// where it was put.
pub(all) enum Admission {
  Admitted
  NotHere
  Refused(String)
} derive(Debug, Eq)

///|
/// The ten flat shapes a slot's admission table is written over.
priv enum Kind {
  KConst
  KStrTpl
  KField
  KBind
  KDyn
  KName
  /// A bare Uppercase name: a component TYPE.
  ///
  /// In NO group, which is the point. A type is not a value — it has no path,
  /// nothing can be read off it, and it cannot be rendered — so it belongs to
  /// no value position and a `@on.click="addItem JsonSelector"` is a parse
  /// error rather than an argument that silently arrives as null. A handler
  /// that needs one asks for it by name (`ctx.make`), with the name declared
  /// in `lookup` where a checker can still see it and the scope it resolves
  /// against.
  ///
  /// The kind stays so that such a token still PARSES far enough to be
  /// reported as what it is.
  KType
  KSeq
  KMethod
  /// A name applied to arguments. Admitted in the CONDITION group only: a
  /// condition slot asks a yes/no question, which is what an application
  /// answers, and a value slot spells a call with `$`. Widening it further is
  /// what the expression language does, not this table.
  KApp
  /// `e.` — a read of the event being handled.
  ///
  /// Admitted in the EVENT ARGUMENT group and nowhere else, which is the whole
  /// of what makes it safe to have. A `@text="e.value"` would be a render-time
  /// read of an event that is not happening, and this table is where that is
  /// refused rather than in a special case somewhere.
  KEvent
}

///|
/// The flat shape of an expression, for the admission table. `None` for a form
/// the table has no row for, which is a form no slot admits.
fn kind_of(val : Expr) -> Kind? {
  match val {
    ELit(..) => Some(KConst)
    // A placeholderless, non-macro template is a literal written the long
    // way and takes the single literal kind; a real placeholder makes it a
    // dynamic template confined to text/all contexts.
    ETpl(..) => if val.is_literal() { Some(KConst) } else { Some(KStrTpl) }
    ERead(place~, ..) =>
      match (place.root, place.steps) {
        (PState(_), []) => Some(KField)
        (PBind(_), []) | (PBind(_), [PField(_)]) => Some(KBind)
        (PState(_), [PIndex(_)]) =>
          if val.as_seq_access() is Some(_) {
            Some(KSeq)
          } else {
            None
          }
        _ => None
      }
    EMethod(..) => Some(KMethod)
    EDyn(..) => Some(KDyn)
    ETypeName(..) => Some(KType)
    EName(..) => Some(KName)
    EApp(..) | EChain(..) | EUnary(..) => Some(KApp)
    EEventPath(..) => Some(KEvent)
    EIf(..) | ERef(..) | EMacroVar(..) | EConfigVar(..) => None
  }
}

///|
fn in_position(kind : Kind, pos : Position) -> Bool {
  match pos {
    PosCondition => kind is (KField | KMethod | KBind | KDyn | KConst | KApp)
    PosText => kind is (KField | KMethod | KBind | KDyn | KConst | KStrTpl)
    // `$m` is in every value-read group but never in a path-bearing one: a
    // method result has no addressable path.
    PosComponent => kind is (KField | KSeq | KDyn)
    PosSequence => kind is (KField | KDyn)
    PosProvide => kind is (KField | KSeq)
    PosField => kind is (KField | KMethod | KConst | KSeq)
    PosEventArg =>
      kind is (KField | KMethod | KBind | KDyn | KName | KConst | KEvent)
    // A handler position holds a NAME. `$name` is admitted by the grammar
    // and refused separately in an `@on` — the sigil claims a distinction the
    // dispatch does not have (`@anode.NodeEvent::method_in_event`) — while a
    // trigger genuinely may be either.
    PosEvent | PosTrigger => kind is (KName | KMethod)
    PosMacroAttr =>
      kind
      is (KField | KMethod | KBind | KDyn | KName | KConst | KStrTpl | KSeq)
    // The body is the position the grammar was written for.
    PosBody => true
  }
}

///|
/// Why the slot language cannot hold this place, when it cannot.
///
/// A slot reads ONE level. `.a.b` and `.a[k].b` are the two things it cannot
/// spell — nested reads and nested writes — because a slot's name lookup is
/// unchecked, while a body is checked code where the generator knows every
/// type along the path.
fn place_refusal(place : Place) -> String? {
  match (place.root, place.steps) {
    (PState(_), []) | (PBind(_), []) | (PBind(_), [PField(_)]) => None
    (PBind(_), [PIndex(_)]) =>
      Some("a slot reads a binding's member by name, not by index")
    (PBind(b), _) => Some("a slot reads one member of `@\{b}`, not a path")
    // A slot never has one: `new` is a statement, and a slot holds one
    // expression with no statement to have opened a target.
    (PTarget, _) =>
      Some("`cur` is what a `new` builds into, and a slot has no `new`")
    // `.seq[.key]` is the one indexed read a slot has, and only with a plain
    // field for the key.
    (PState(seq), [PIndex(idx)]) =>
      if idx.as_field() is Some(_) {
        None
      } else {
        Some("a slot indexes by a plain field — write `.\{seq}[.key]`")
      }
    (PState(seq), [PField(m), ..]) =>
      Some(
        "a slot reads one level, so `.\{seq}.\{m}` has nothing to look in — render the child as a component, or name the read with a `compute`",
      )
    (PState(f), _) =>
      Some(
        "a slot reads one level, so `.\{f}` cannot be walked into — render the child as a component, or name the read with a `compute`",
      )
    (PParam(n), _) => Some("`\{n}` is a parameter, and a slot has none")
  }
}

///|
/// May this form stand in this position?
///
/// Total over the AST, and the same answer whether the caller reached the
/// expression through a single token or through the whole grammar — which is
/// the property the two-table version did not have.
pub fn Expr::admitted_in(self : Expr, pos : Position) -> Admission {
  // The body admits the grammar. Everything below is about SLOTS.
  if pos is PosBody {
    return Admitted
  }
  match self {
    ERead(place~, ..) | ERef(place~, ..) =>
      if place_refusal(place) is Some(why) {
        return Refused(why)
      }
    _ => ()
  }
  match self {
    ERef(..) => return Refused("`&` names a position, and a slot reads values")
    EIf(..) =>
      return Refused(
        "a slot has no `if` — `@show` / `@hide` ARE the choice, and a value that picks between two needs `@if.` with `@then` / `@else`",
      )
    EUnary(op=UNeg, ..) =>
      return Refused(
        "a slot has no arithmetic — name the value with a `compute`",
      )
    // The operator words ARE the builtin names, so a family the slot
    // vocabulary has no row for is named rather than silently evaluated to
    // nothing.
    EChain(ops~, ..) =>
      for op in ops {
        if builtin(op) is None {
          return Refused("`\{op}` is not a slot operator")
        }
      }
    // A placeholderless template is a literal written the long way, and takes
    // the literal's kind — so `@show="$'flex'"` is the constant it looks like.
    // One with a real placeholder is TEXT, and a condition is not text.
    ETpl(..) =>
      if pos is PosCondition && !self.is_literal() {
        return Refused("a template is text, and a condition is not text")
      }
    _ => ()
  }
  match kind_of(self) {
    Some(k) => if in_position(k, pos) { Admitted } else { NotHere }
    None => NotHere
  }
}