Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ object `package` extends ScalaModule with ScoverageModule with SonatypeCentralPu
override def scalacOptions = Seq(
"-deprecation",
"-feature",
// "-explain",
"-explain",
"-new-syntax",
"-unchecked",
"-language:noAutoTupling",
"-Vprofile",
"-Xprint-inline",
// "-Ycheck:all", // cannot be enabled when scoverage used :/
// "-Ycheck:all", // cannot be enabled when scoverage used :/
"-Ycheck:macros",
// "-Ydebug-error",
"-Ydebug-flags",
Expand Down
4 changes: 2 additions & 2 deletions src/alpaca/internal/Csv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ private[internal] object Csv:
* @return a Csv representation of the named tuples
*/

inline def toCsv: Csv =
inline def toCsv(using DebugSettings): Csv =
Csv(
compiletime.constValueTuple[N].toShowableList,
rows.map(_.toTuple.toShowableList),
)

extension [T <: Tuple](tuple: T)
inline private def toShowableList = compiletime
inline private def toShowableList(using DebugSettings) = compiletime
.summonAll[Tuple.Map[T, Showable]]
.zip(tuple)
.toList
Expand Down
6 changes: 5 additions & 1 deletion src/alpaca/internal/ValidName.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package alpaca
package internal

import compiletime.ops.any.ToString

/**
* Type alias for valid token names.
*
Expand All @@ -11,8 +13,10 @@ package internal
//todo: make it opaque with ban on underscore
type ValidName = String & Singleton

object ValidName:
//todo: find better name
type ValidNameLike = (Char | String) & Singleton

object ValidName:
def from[Name <: ValidName: Type](using quotes: Quotes)(using DebugSettings): ValidName =
import quotes.reflect.*
TypeRepr.of[Name] match
Expand Down
41 changes: 30 additions & 11 deletions src/alpaca/internal/internal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,28 @@ private[internal] final class ReplaceRefs[Q <: Quotes](using val quotes: Q):
* @param queries pairs of (symbol to find, replacement term)
* @return a TreeMap that performs the replacements
*/
def apply(queries: (find: Symbol, replace: Term)*): TreeMap = new TreeMap:
// skip NoSymbol
private val filtered = queries.view.filterNot(_.find.isNoSymbol)
def apply(queries: (find: Tree, replace: Term)*): TreeMap = new TreeMap:
private val filtered = queries.view
.collect:
case (find, replace) if !find.symbol.isNoSymbol => (find.symbol, replace)
.toMap

private val singletons: Map[Constant | Null, Term] = queries.view
.collect:
case (Bind(_, Literal(term)), replace) => (term, replace)
case (Literal(term), replace) => (term, replace)
.toMap

override def transformTerm(tree: Term)(owner: Symbol): Term =
val term = tree match
case Bind(_, Literal(term)) => term
case Literal(term) => term
case _ => null

filtered
.collectFirst:
case (find, replace) if find == tree.symbol => replace
.get(tree.symbol)
.orElse(singletons.get(term))
.map(_.changeOwner(owner))
.getOrElse(super.transformTerm(tree)(owner))

/**
Expand All @@ -48,8 +62,8 @@ private[internal] final class ReplaceRefs[Q <: Quotes](using val quotes: Q):
* @tparam Q the Quotes type
* @param quotes the Quotes instance
*/
private[internal] final class CreateLambda[Q <: Quotes](using val quotes: Q)(using DebugSettings):

private[internal] final class CreateLambda[Q <: Quotes](using val quotes: Q)(using DebugSettings):
import quotes.reflect.*

/**
Expand All @@ -67,7 +81,7 @@ private[internal] final class CreateLambda[Q <: Quotes](using val quotes: Q)(usi
Lambda(
Symbol.spliceOwner,
MethodType(params.zipWithIndex.map((_, i) => s"$$arg$i"))(_ => params, _ => r),
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeOwner(sym) call is added to the result of rhsFn.unsafeApply. While this may be necessary for correct symbol ownership in the macro-generated code, ensure this doesn't break existing functionality where the owner was previously not changed. If this fixes a bug, consider documenting why this change is needed.

Suggested change
MethodType(params.zipWithIndex.map((_, i) => s"$$arg$i"))(_ => params, _ => r),
MethodType(params.zipWithIndex.map((_, i) => s"$$arg$i"))(_ => params, _ => r),
// Re-own the body tree under the synthetic method symbol `sym` created by `Lambda`.
// `rhsFn` typically builds its tree under `Symbol.spliceOwner`; without `changeOwner(sym)`
// the resulting lambda body would have an incorrect owner and could produce invalid trees.

Copilot uses AI. Check for mistakes.
(sym, args) => rhsFn.unsafeApply((sym, args))(using Default[Expr[?]].transform(_.asTerm)),
(sym, args) => rhsFn.unsafeApply((sym, args))(using Default[Expr[?]].transform(_.asTerm)).changeOwner(sym),
).asExprOf[F]

private[internal] given [K <: Tuple, V <: Tuple: ToExpr]: ToExpr[NamedTuple[K, V]] with
Expand All @@ -79,15 +93,20 @@ private[internal] given [T: ToExpr as toExpr]: ToExpr[T | Null] with
case value => toExpr(value.asInstanceOf[T])

// todo: it's temporary, remove when we have a proper timeout implementation
inline private[internal] def withDebugSettings[T](inline block: DebugSettings ?=> T)(using Quotes): T =
inline private[alpaca] def withDebugSettings[T](inline block: DebugSettings ?=> T): T =
${ withDebugSettingsImpl('{ block }) }

def withDebugSettingsImpl[T: Type](block: Expr[DebugSettings ?=> T])(using Quotes): Expr[T] =
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*
import scala.concurrent.{Await, Future}

given DebugSettings = Expr.summon[DebugSettings].get.valueOrAbort
val debugSettings = Expr.summon[DebugSettings].get

val future = Future(block)
Await.result(future, summon[DebugSettings].timeout.seconds)
'{
val future = Future($block(using $debugSettings))
Await.result(future, $debugSettings.timeout.seconds)
}

private[internal] given FromExpr[DebugSettings] with

Expand Down
58 changes: 42 additions & 16 deletions src/alpaca/internal/lexer/CompileNameAndPattern.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ import scala.annotation.tailrec
private[lexer] final class CompileNameAndPattern[Q <: Quotes](using val quotes: Q)(using DebugSettings):
import quotes.reflect.*

private def extractOrTypes(tree: Tree, level: Int = 0): List[Tree] = tree match
case tree @ Applied(tpt, List(left, right)) if tpt.tpe =:= TypeRepr.of[|] =>
s"Level $level: ${tree.toString}\n".soft
extractOrTypes(left, level + 1) ++ extractOrTypes(right, level + 1)
case tree @ Typed(_, pattern) =>
s"Level $level: ${tree.toString}\n".soft
extractOrTypes(pattern, level + 1)
case pattern =>
s"Final Level $level: ${pattern.toString}\n".soft
List(pattern)

def treeToStr(tree: Tree): String | Char = tree match
case Literal(StringConstant(str)) => str
case Singleton(Literal(StringConstant(str))) => str
case Literal(CharConstant(char)) => char
case Singleton(Literal(CharConstant(char))) => char
case x => raiseShouldNeverBeCalled[String](x.toString)

/**
* Compiles a pattern tree into token information.
*
Expand All @@ -27,38 +45,46 @@ private[lexer] final class CompileNameAndPattern[Q <: Quotes](using val quotes:
* @param pattern the pattern tree to compile
* @return a list of TokenInfo expressions
*/
def apply[T: Type](pattern: Tree): List[Expr[TokenInfo[?]]] =
def apply[T <: ValidNameLike: Type](pattern: Tree): List[Expr[TokenInfo[?]]] =
@tailrec def loop(tpe: TypeRepr, pattern: Tree): List[Expr[TokenInfo[?]]] =
(tpe, pattern) match
// case x @ "regex" => Token[x.type]
case (TermRef(qual, name), Bind(bind, Literal(StringConstant(regex)))) if name == bind =>
TokenInfo.unsafe(regex, regex) :: Nil
// case x @ ("regex" | "regex2") => Token[x.type]
TokenInfo.unsafe(regex, Vector(regex)) :: Nil
// case x @ 'l' => Token[x.type]
case (TermRef(qual, name), Bind(bind, Literal(CharConstant(regex)))) if name == bind =>
TokenInfo.unsafe(regex.toString, Vector(regex)) :: Nil
// case x @ ("regex" | 'l') => Token[x.type]
case (TermRef(qual, name), Bind(bind, Alternatives(alternatives))) if name == bind =>
alternatives.unsafeMap:
case Literal(StringConstant(str)) => TokenInfo.unsafe(str, str)
alternatives.map(treeToStr).map(str => TokenInfo.unsafe(str.toString, Vector(str)))
// case x @ <?> => Token[<?>]
case (tpe, Bind(_, tree)) =>
loop(tpe, tree)
// case x : "regex" => Token.Ignored
case (tpe, Literal(StringConstant(str))) if tpe =:= TypeRepr.of[Nothing] =>
TokenInfo.unsafe(str, str) :: Nil
// case x : ("regex" | "regex2") => Token.Ignored
TokenInfo.unsafe(str, Vector(str)) :: Nil
// case x : 'l' => Token.Ignored
case (tpe, Literal(CharConstant(str))) if tpe =:= TypeRepr.of[Nothing] =>
TokenInfo.unsafe(str.toString, Vector(str)) :: Nil
// case x : ("regex" | 'l') => Token.Ignored
case (tpe, Alternatives(alternatives)) if tpe =:= TypeRepr.of[Nothing] =>
alternatives.unsafeMap:
case Literal(StringConstant(str)) => TokenInfo.unsafe(str, str)
alternatives.map(treeToStr).map(str => TokenInfo.unsafe(str.toString, Vector(str)))
// case x : "regex" => Token["name"]
case (ConstantType(StringConstant(name)), Literal(StringConstant(regex))) =>
TokenInfo.unsafe(name, regex) :: Nil
// case x : ("regex" | "regex2") => Token["name"]
case (ConstantType(StringConstant(str)), Alternatives(alternatives)) =>
TokenInfo.unsafe(name, Vector(regex)) :: Nil
// case x : 'l' => Token["name"]
case (ConstantType(StringConstant(name)), Literal(CharConstant(regex))) =>
TokenInfo.unsafe(name, Vector(regex)) :: Nil
// case x : ("regex" | 'l') => Token["name"]
case (ConstantType(StringConstant(str)), alternatives) =>
TokenInfo.unsafe(
str,
alternatives
.unsafeMap:
case Literal(StringConstant(str)) => str
.mkString("|"),
extractOrTypes(alternatives).map(treeToStr).toVector,
) :: Nil
// case x: ("regex" | 'l') => Token[x.type]
case (TermRef(qual, name), alternatives) =>
extractOrTypes(alternatives).map(treeToStr).map(str => TokenInfo.unsafe(str.toString, Vector(str)))

case x => raiseShouldNeverBeCalled[List[Expr[TokenInfo[?]]]](x.toString)

loop(TypeRepr.of[T], pattern)
Loading
Loading