<commit_before>/*
    Contextual, version 0.4.0. Copyright 2016-22 <NAME>, Propensive OÜ.

    The primary distribution site is: https://propensive.com/

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
    file except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the
    License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    either express or implied. See the License for the specific language governing permissions
    and limitations under the License.
*/

package contextual

import scala.quoted.*
import scala.compiletime.*

import rudiments.*

case class InterpolationError(message: Text, offset: Maybe[Int] = Unset, length: Maybe[Int] = Unset) extends Error

trait Interpolator[Input, State, Result]:

  given CanThrow[InterpolationError] = compiletime.erasedValue

  protected def initial: State
  protected def parse(state: State, next: Text): State
  protected def skip(state: State): State
  protected def substitute(state: State, value: Text): State = parse(state, value)
  protected def insert(state: State, value: Input): State
  protected def complete(value: State): Result

  def expand(target: Expr[Interpolator[Input, State, Result]], ctx: Expr[StringContext],
                 seq: Expr[Seq[Any]])
            (using Quotes, Type[Input], Type[State], Type[Result]): Expr[Result] =
    import quotes.reflect.*

    def shift(pos: Position, offset: Int, length: Int): Position =
      Position(pos.sourceFile, pos.start + offset, pos.start + offset + length)

    def rethrow[T](blk: => T, pos: Position): T =
      import unsafeExceptions.canThrowAny
      try blk catch case err: InterpolationError =>
        err match
          case InterpolationError(msg, offset, length) =>
            throw PositionalError(msg, shift(pos, offset.otherwise(0),
                length.otherwise(pos.end - pos.start - offset.otherwise(0))))

    case class PositionalError(message: Text, position: Position) extends Error
    
    def recur(seq: Seq[Expr[Any]], parts: Seq[String], positions: Seq[Position], state: State,
                  expr: Expr[State]): Expr[Result] =
      seq match
        case '{ $head: h } +: tail =>

          def notFound: Nothing =
            val typeName: String = TypeRepr.of[h].widen.show
            
            report.errorAndAbort(
              s"contextual: can't substitute $typeName into this interpolated string",<commit_msg>Start using rich exception messages<commit_after>/*
    Contextual, version 0.4.0. Copyright 2016-22 <NAME>, Propensive OÜ.

    The primary distribution site is: https://propensive.com/

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
    file except in compliance with the License. You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the
    License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    either express or implied. See the License for the specific language governing permissions
    and limitations under the License.
*/

package contextual

import scala.quoted.*
import scala.compiletime.*

import rudiments.*

case class InterpolationError(message: Text, offset: Maybe[Int] = Unset, length: Maybe[Int] = Unset)
extends Error((Text("$message at "), offset, Text("-"), length))

trait Interpolator[Input, State, Result]:

  given CanThrow[InterpolationError] = compiletime.erasedValue

  protected def initial: State
  protected def parse(state: State, next: Text): State
  protected def skip(state: State): State
  protected def substitute(state: State, value: Text): State = parse(state, value)
  protected def insert(state: State, value: Input): State
  protected def complete(value: State): Result

  def expand(target: Expr[Interpolator[Input, State, Result]], ctx: Expr[StringContext],
                 seq: Expr[Seq[Any]])
            (using Quotes, Type[Input], Type[State], Type[Result]): Expr[Result] =
    import quotes.reflect.*

    def shift(pos: Position, offset: Int, length: Int): Position =
      Position(pos.sourceFile, pos.start + offset, pos.start + offset + length)

    def rethrow[T](blk: => T, pos: Position): T =
      import unsafeExceptions.canThrowAny
      try blk catch case err: InterpolationError =>
        err match
          case InterpolationError(msg, offset, length) =>
            throw PositionalError(msg, shift(pos, offset.otherwise(0),
                length.otherwise(pos.end - pos.start - offset.otherwise(0))))

    case class PositionalError(message: Text, position: Position) extends Error(EmptyTuple)
    
    def recur(seq: Seq[Expr[Any]], parts: Seq[String], positions: Seq[Position], state: State,
                  expr: Expr[State]): Expr[Result] =
      seq match
        case '{ $head: h } +: tail =>

          def notFound: Nothing =
            val typeName: String = TypeRepr.of[h].widen.show
            
            report.errorAndAbort(
              s"contextual: can't substitute $typeName into this interpolated string",
{'fraction_non_alphanumeric': 0.1011826544021025, 'fraction_numerical': 0.005631687629059508, 'mean_word_length': 3.3707957342083676, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4089360', 'n_tokens_mistral': 1476, 'n_tokens_neox': 1434, 'n_words': 521}