diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index a0fb99651..4bede7095 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -114,6 +114,7 @@ class JSBuilder(using Elaborator.State) extends CodeBuilder: }" case Instantiate(cls, as) => doc"new ${result(cls)}(${as.map(result).mkDocument(", ")})" + case Value.Arr(es) if es.isEmpty => doc"[]" case Value.Arr(es) => doc"[ #{ # ${es.map(result).mkDocument(doc", # ")} #} # ]" def returningTerm(t: Block)(using Raise, Scope): Document = t match @@ -190,7 +191,7 @@ class JSBuilder(using Elaborator.State) extends CodeBuilder: } + ")"""" }; }""" } #} # }" - if (clsDefn.kind is syntax.Mod) || (clsDefn.kind is syntax.Obj) then + if ((clsDefn.kind is syntax.Mod) || (clsDefn.kind is syntax.Obj)) || (clsDefn.kind is syntax.Pat) then val clsTmp = summon[Scope].allocateName(new semantics.TempSymbol(N, sym.nme+"$"+"class")) clsDefn.owner match case S(owner) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Desugarer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Desugarer.scala index 7c8773dff..a381bdba6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Desugarer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Desugarer.scala @@ -9,6 +9,7 @@ import hkmc2.syntax.Literal import Keyword.{as, and, `else`, is, let, `then`} import collection.mutable.HashMap import Elaborator.{ctx, Ctxl} +import ucs.DesugaringBase object Desugarer: extension (op: Keyword.Infix) @@ -29,8 +30,8 @@ object Desugarer: val tupleLast: HashMap[Int, BlockLocalSymbol] = HashMap.empty end Desugarer -class Desugarer(tl: TraceLogger, elaborator: Elaborator) - (using raise: Raise, state: Elaborator.State, c: Elaborator.Ctx): +class Desugarer(tl: TraceLogger, val elaborator: Elaborator) + (using raise: Raise, state: Elaborator.State, c: Elaborator.Ctx) extends DesugaringBase: import Desugarer.* import Elaborator.Ctx import elaborator.term @@ -359,12 +360,6 @@ class Desugarer(tl: TraceLogger, elaborator: Elaborator) raise(ErrorReport(msg"Unrecognized pattern split." -> tree.toLoc :: Nil)) _ => _ => Split.default(Term.Error) - private lazy val tupleSlice = - term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("tupleSlice"))) - - private lazy val tupleGet = - term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("tupleGet"))) - /** Elaborate a single match (a scrutinee and a pattern) and forms a split * with an innermost split as the sequel of the match. * @param scrutSymbol the symbol representing the scrutinee @@ -395,6 +390,7 @@ class Desugarer(tl: TraceLogger, elaborator: Elaborator) Branch(ref, Pattern.ClassLike(cls, clsTrm, N, false)(ctor), sequel(ctx)) ~: fallback case S(cls: ModuleSymbol) => Branch(ref, Pattern.ClassLike(cls, clsTrm, N, false)(ctor), sequel(ctx)) ~: fallback + case S(psym: PatternSymbol) => makeUnapplyBranch(ref, psym, sequel(ctx))(fallback) case N => // Raise an error and discard `sequel`. Use `fallback` instead. raise(ErrorReport(msg"Cannot use this ${ctor.describe} as a pattern" -> ctor.toLoc :: Nil)) @@ -412,10 +408,6 @@ class Desugarer(tl: TraceLogger, elaborator: Elaborator) case ((lead, N), pat) => (lead :+ pat, N) case ((lead, S((rest, last))), pat) => (lead, S((rest, last :+ pat))) // Some helper functions. TODO: deduplicate - def int(i: Int) = Term.Lit(IntLit(BigInt(i))) - def fld(t: Term) = Fld(FldFlags.empty, t, N) - def tup(xs: Fld*) = Term.Tup(xs.toList)(Tup(Nil)) - def app(lhs: Term, rhs: Term, sym: FlowSymbol) = Term.App(lhs, rhs)(Tree.App(Tree.Empty(), Tree.Empty()), sym) def getLast(i: Int) = TempSymbol(N, s"last$i") // `wrap`: add let bindings for tuple elements // `matches`: pairs of patterns and symbols to be elaborated @@ -426,7 +418,7 @@ class Desugarer(tl: TraceLogger, elaborator: Elaborator) case ((wrapInner, matches), (pat, lastIndex)) => val sym = scrutSymbol.getTupleLastSubScrutinee(lastIndex) val wrap = (split: Split) => - Split.Let(sym, app(tupleGet, tup(fld(ref), fld(int(-1 - lastIndex))), sym), wrapInner(split)) + Split.Let(sym, callTupleGet(ref, -1 - lastIndex, sym), wrapInner(split)) (wrap, (sym, pat) :: matches) val lastMatches = reversedLastMatches.reverse rest match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index eb7175bd5..8334a6e4f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -109,8 +109,18 @@ extends Importer: def mkLetBinding(sym: LocalSymbol, rhs: Term): Ls[Statement] = LetDecl(sym) :: DefineVar(sym, rhs) :: Nil - def resolveField(srcTree: Tree, base: Opt[Symbol], nme: Ident): Opt[Symbol] = + def resolveField(srcTree: Tree, base: Opt[Symbol], nme: Ident): Ctxl[Opt[Symbol]] = base match + // Look up symbols qualified by `globalThis.`. + case S(tsym: TopLevelSymbol) => + // Locate the nearest context with top-level symbols. + def find(ctx: Ctx): Ctx = + ctx.outer match + case S(sym: TopLevelSymbol) => ctx + case _ => ctx.parent match + case S(pctx) => find(pctx) + case N => ctx + find(ctx).get(nme.name).flatMap(_.symbol) case S(psym: BlockMemberSymbol) => psym.modTree match case S(cls) => @@ -606,7 +616,7 @@ extends Importer: raise(d) go(sts, acc) case (td @ TypeDef(k, head, extension, body)) :: sts => - assert((k is Als) || (k is Cls) || (k is Mod) || (k is Obj), k) + assert((k is Als) || (k is Cls) || (k is Mod) || (k is Obj) || (k is Pat), k) val nme = td.name match case R(id) => id case L(d) => @@ -658,6 +668,17 @@ extends Importer: semantics.TypeDef(alsSym, tps, extension.map(term), N) alsSym.defn = S(d) d + case Pat => + val patSym = td.symbol.asInstanceOf[PatternSymbol] // TODO improve `asInstanceOf` + val owner = ctx.outer + newCtx.nest(S(patSym)).givenIn: + assert(body.isEmpty) + log(s"pattern body is ${td.extension}") + val compose = new ucs.Translator(tl, this) + val bod = compose(ps.getOrElse(Nil), td.extension.getOrElse(die)) + val pd = PatternDef(owner, patSym, tps, ps, ObjBody(Term.Blk(bod, Term.Lit(UnitLit(true))))) + patSym.defn = S(pd) + pd case k: (Mod.type | Obj.type) => val clsSym = td.symbol.asInstanceOf[ModuleSymbol] // TODO: improve `asInstanceOf` val owner = ctx.outer diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index 6d9ad4109..04e6d6966 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -36,13 +36,19 @@ abstract class Symbol(using State) extends Located: case mem: BlockMemberSymbol => mem.modTree.map(_.symbol.asInstanceOf[ModuleSymbol]) case _ => N + def asPat: Opt[PatternSymbol] = this match + case pat: PatternSymbol => S(pat) + case mem: BlockMemberSymbol => + mem.patTree.map(_.symbol.asInstanceOf[PatternSymbol]) + case _ => N def asAls: Opt[TypeAliasSymbol] = this match case cls: TypeAliasSymbol => S(cls) case mem: BlockMemberSymbol => mem.alsTree.map(_.symbol.asInstanceOf[TypeAliasSymbol]) case _ => N - def asClsLike: Opt[ClassSymbol | ModuleSymbol] = asCls orElse asMod + def asClsLike: Opt[ClassSymbol | ModuleSymbol | PatternSymbol] = + (asCls: Opt[ClassSymbol | ModuleSymbol | PatternSymbol]) orElse asMod orElse asPat def asTpe: Opt[TypeSymbol] = asCls orElse asAls override def equals(x: Any): Bool = x match @@ -102,6 +108,8 @@ class BlockMemberSymbol(val nme: Str, val trees: Ls[Tree])(using State) case t: Tree.TypeDef if (t.k is Mod) || (t.k is Obj) => t def alsTree: Opt[Tree.TypeDef] = trees.collectFirst: case t: Tree.TypeDef if t.k is Als => t + def patTree: Opt[Tree.TypeDef] = trees.collectFirst: + case t: Tree.TypeDef if t.k is Pat => t def trmTree: Opt[Tree.TermDef] = trees.collectFirst: case t: Tree.TermDef /* if t.k is */ => t def trmImplTree: Opt[Tree.TermDef] = trees.collectFirst: @@ -172,6 +180,12 @@ class TypeAliasSymbol(val id: Tree.Ident)(using State) extends MemberSymbol[Type def toLoc: Option[Loc] = id.toLoc // TODO track source tree of type alias here override def toString: Str = s"module:${id.name}" +class PatternSymbol(val id: Tree.Ident)(using State) + extends MemberSymbol[PatternDef] with CtorSymbol with InnerSymbol: + def nme = id.name + def toLoc: Option[Loc] = id.toLoc // TODO track source tree of pattern here + override def toString: Str = s"pattern:${id.name}" + class TopLevelSymbol(blockNme: Str)(using State) extends MemberSymbol[ModuleDef] with InnerSymbol: def nme = blockNme diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 36e4bcbbc..a9a858382 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -113,6 +113,8 @@ sealed trait Statement extends AutoLocated: mod.paramsOpt.toList.flatMap(_.flatMap(_.subTerms)) ::: mod.body.blk :: Nil case td: TypeDef => td.rhs.toList + case pat: PatternDef => + pat.paramsOpt.toList.flatMap(_.flatMap(_.subTerms)) ::: pat.body.blk :: Nil case Import(sym, pth) => Nil case Try(body, finallyDo) => body :: finallyDo :: Nil case Handle(lhs, rhs, defs) => rhs :: defs._1 :: Nil @@ -247,6 +249,16 @@ case class ModuleDef( body: ObjBody, ) extends ClassLikeDef with Companion +case class PatternDef( + owner: Opt[InnerSymbol], + sym: PatternSymbol, + tparams: Ls[TyParam], + paramsOpt: Opt[Ls[Param]], + body: ObjBody +) extends ClassLikeDef: + self => + val kind: ClsLikeKind = Pat + sealed abstract class ClassDef extends ClassLikeDef: val kind: ClsLikeKind diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/DesugaringBase.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/DesugaringBase.scala new file mode 100644 index 000000000..b9ffc2104 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/DesugaringBase.scala @@ -0,0 +1,122 @@ +package hkmc2 +package semantics +package ucs + +import mlscript.utils.*, shorthands.* +import syntax.Tree.*, Elaborator.Ctxl + +/** Contains some helpers that makes UCS desugaring easier. */ +trait DesugaringBase(using Elaborator.State): + val elaborator: Elaborator + + import elaborator.{term, cls} + + protected transparent inline def int(i: Int) = Term.Lit(IntLit(BigInt(i))) + protected transparent inline def fld(t: Term) = Fld(FldFlags.empty, t, N) + protected transparent inline def tup(xs: Fld*) = Term.Tup(xs.toList)(Tup(Nil)) + protected transparent inline def app(lhs: Term, rhs: Term, sym: FlowSymbol) = + Term.App(lhs, rhs)(App(Empty(), Empty()), sym) + + protected lazy val matchResultClass = + Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("MatchResult")) + + protected lazy val matchResultFailure = + Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("MatchFailure")) + + protected lazy val tupleSlice: Ctxl[Term] = + term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("tupleSlice"))) + + protected lazy val tupleGet: Ctxl[Term] = + term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("tupleGet"))) + + protected lazy val stringStartsWith: Ctxl[Term] = + term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("stringStartsWith"))) + + protected lazy val stringGet: Ctxl[Term] = + term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("stringGet"))) + + protected lazy val stringDrop: Ctxl[Term] = + term(Sel(Sel(Ident("globalThis"), Ident("Predef")), Ident("stringDrop"))) + + protected def callTupleGet(t: Term, i: Int, s: FlowSymbol): Ctxl[Term] = + app(tupleGet, tup(fld(t), fld(int(i))), s) + + protected def callStringStartsWith(t: Term, prefix: Term, s: FlowSymbol): Ctxl[Term] = + app(stringStartsWith, tup(fld(t), fld(prefix)), s) + + protected def callStringGet(t: Term, i: Int, s: FlowSymbol): Ctxl[Term] = + app(stringGet, tup(fld(t), fld(int(i))), s) + + protected def callStringDrop(t: Term, n: Int, s: FlowSymbol): Ctxl[Term] = + app(stringDrop, tup(fld(t), fld(int(n))), s) + + protected transparent inline def tempLet(term: Term)(inner: TempSymbol => Split): Split = + val s = TempSymbol(N, "temp") + Split.Let(s, term, inner(s)) + + protected transparent inline def plainTest(cond: Term, dbgName: Str = "cond")(inner: => Split): Split = + val s = TempSymbol(N, dbgName) + Split.Let(s, cond, Branch(s.ref(), inner) ~: Split.End) + + /** Make a `Branch` that calls `Pattern` symbols' `unapply` functions. */ + def makeUnapplyBranch( + scrut: => Term.Ref, + psym: PatternSymbol, + inner: => Split, + method: Str = "unapply" + )(fallback: Split): Ctxl[Split] = + val matchResultClassTerm = cls(matchResultClass) + matchResultClassTerm.symbol match + case S(matchResultClassSymbol: ClassSymbol) => + // def makeUnapplyBranch(scrut: => Term.Ref, pat) + val resultIdent = Ident("matchResult"): Ident + val resultSymbol = TempSymbol(N, "matchResult") + val globalThis = term(Ident("globalThis")) + val unapply = Term.Sel( + prefix = Term.Sel(globalThis, Ident(psym.nme))(S(psym)), + nme = Ident(method) + )(N) + val arguments = Term.Tup(Fld(FldFlags.empty, scrut, N) :: Nil)(Tup(Nil)) + val call = Term.App(unapply, arguments)(App(Empty(), Empty()), FlowSymbol(s"result of $method")) + Split.Let(resultSymbol, call, + Branch( + resultSymbol.ref(), + Pattern.ClassLike(matchResultClassSymbol, matchResultClassTerm, N, false)(matchResultClass), + inner + ) ~: fallback) + case S(_) | N => lastWords("Cannot locate `MatchResult` class in the global scope.") + + /** Make a `Branch` that calls `Pattern` symbols' `unapplyStringPrefix` functions. */ + def makeUnapplyStringPrefixBranch( + scrut: => Term.Ref, + psym: PatternSymbol, + inner: (scrut: TempSymbol) => Split, + method: Str = "unapplyStringPrefix" + )(fallback: Split): Ctxl[Split] = + val matchResultClassTerm = cls(matchResultClass) + matchResultClassTerm.symbol match + case S(matchResultClassSymbol: ClassSymbol) => + // def makeUnapplyBranch(scrut: => Term.Ref, pat) + val resultIdent = Ident("matchResult"): Ident + val resultSymbol = TempSymbol(N, "matchResult") + val globalThis = term(Ident("globalThis")) + val unapply = Term.Sel( + prefix = Term.Sel(globalThis, Ident(psym.nme))(S(psym)), + nme = Ident(method) + )(N) + val arguments = Term.Tup(Fld(FldFlags.empty, scrut, N) :: Nil)(Tup(Nil)) + val call = Term.App(unapply, arguments)(App(Empty(), Empty()), FlowSymbol(s"result of $method")) + tempLet(call): resultSymbol => + val argSym = TempSymbol(N, "arg") + val tupleGetRes = FlowSymbol("postfix") + Branch( + resultSymbol.ref(), + Pattern.ClassLike( + matchResultClassSymbol, + matchResultClassTerm, + S(argSym :: Nil), + false + )(matchResultClass), + tempLet(callTupleGet(argSym.ref(), 0, tupleGetRes))(inner) + ) ~: fallback + case S(_) | N => lastWords("Cannot locate `MatchResult` class in the global scope.") diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Translator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Translator.scala new file mode 100644 index 000000000..fe9c4ab95 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Translator.scala @@ -0,0 +1,259 @@ +package hkmc2 +package semantics +package ucs + +import syntax.{Keyword, Tree}, Tree.* +import mlscript.utils.*, shorthands.* +import Message.MessageContext +import utils.TraceLogger +import syntax.Literal +import Keyword.{as, and, `else`, is, let, `then`} +import collection.mutable.HashMap +import Elaborator.{ctx, Ctxl} +import semantics.Split.display +import semantics.ucs.Normalization +import syntax.{Fun, Literal, ParamBind} +import scala.collection.mutable.Buffer + +object Translator: + object or: + infix def unapply(tree: Tree): Opt[(Tree, Tree)] = tree match + case App(Ident("|"), Tup(lhs :: rhs :: Nil)) => S(lhs, rhs) + case _ => N + object to: + infix def unapply(tree: Tree): Opt[(Tree, (Bool, Tree))] = tree match + case App(Ident("..="), Tup(lhs :: rhs :: Nil)) => S(lhs, (true, rhs)) + case App(Ident("..<"), Tup(lhs :: rhs :: Nil)) => S(lhs, (false, rhs)) + case _ => N + +class Translator(tl: TraceLogger, val elaborator: Elaborator) + (using raise: Raise, state: Elaborator.State, c: Elaborator.Ctx) extends DesugaringBase: + import tl.*, Translator.*, elaborator.term + + extension (split: Split) + def ~~:(fallback: Split): Split = + if fallback == Split.End then + split + else if split.isFull then + split + else (split match + case Split.Cons(head, tail) => Split.Cons(head, tail ~~: fallback) + case Split.Let(name, term, tail) => Split.Let(name, term, tail ~~: fallback) + case Split.Else(_) /* impossible */ | Split.End => fallback) + + private def error(msgs: (Message, Option[Loc])*): Unit = + raise(ErrorReport(msgs.toList)) + + private def warn(msgs: (Message, Option[Loc])*): Unit = + raise(WarningReport(msgs.toList)) + + + private def decompose(tree: Tree): ::[Tree] = trace( + pre = s"decompose <<< $tree", + post = (ms: ::[Tree]) => s"decompose >>> ${ms.mkString(", ")}" + ): + def go(tree: Tree, acc: Tree => ::[Tree]): () => ::[Tree] = tree match + case lhs or rhs => go(lhs, ::(_, go(rhs, acc)())) + case item => () => acc(item) + go(tree, ::(_, Nil))() + + private type Scrut = () => Term.Ref + + private type CaptureMap = Map[Param, Term.Ref] + + private type Inner = CaptureMap => Split + + private type PostfixInner = (CaptureMap, Scrut) => Split + + import Branch as B, Pattern as P, Split as Sp + + private def matchResult(captures: Term) = + val callee = term(matchResultClass) + val params = Term.Tup(Fld(FldFlags.empty, captures, N) :: Nil)(Tup(Nil)) + Term.App(callee, params)(App(matchResultClass, Tree.Empty()), FlowSymbol("result of match")) + + private def matchFailure() = + val callee = term(matchResultFailure) + Term.App(callee, Term.Tup(Nil)(Tup(Nil)))(App(matchResultFailure, Tree.Empty()), FlowSymbol("result of match")) + + private def makeRange(scrut: Scrut, lo: Literal, hi: Literal, rightInclusive: Bool, inner: Inner) = + val op1 = term(Ident("<=")) + val argLo = Fld(FldFlags.empty, Term.Lit(lo), N) + val argHi = Fld(FldFlags.empty, Term.Lit(hi), N) + def argScrut = Fld(FldFlags.empty, scrut(), N) + val gtLoRes = FlowSymbol("gtLo") + val gtLo = Term.App(op1, Term.Tup(argLo :: argScrut :: Nil)(Tup(Nil)))(App(Empty(), Empty()), gtLoRes) + val ltHiRes = FlowSymbol("ltHi") + val op2 = if rightInclusive then op1 else term(Ident("<")) + val ltHi = Term.App(op2, Term.Tup(argScrut :: argHi :: Nil)(Tup(Nil)))(App(Empty(), Empty()), ltHiRes) + plainTest(gtLo, "gtLo"): + plainTest(ltHi, "ltHi"): + inner(Map.empty) + + /** Generate a split that consumes the entire scrutinee. */ + private def full(scrut: Scrut, pat: Tree, inner: Inner): Sp = trace( + pre = s"full <<< $pat", + post = (split: Sp) => s"full >>> $split" + ): + pat match + case lhs or rhs => full(scrut, lhs, inner) ~~: full(scrut, rhs, inner) + case (lo: StrLit) to (incl, hi: StrLit) => + // String range bounds must be single characters. + val ds = Buffer.empty[(Message, Option[Loc])] + if lo.value.length != 1 then + ds += msg"String range bounds must have only one character." -> lo.toLoc + if hi.value.length != 1 then + ds += msg"String range bounds must have only one character." -> hi.toLoc + if ds.nonEmpty then + error(ds.toSeq*) + failure + else + makeRange(scrut, lo, hi, incl, inner) + case (lo: IntLit) to (incl, hi: IntLit) => makeRange(scrut, lo, hi, incl, inner) + case (lo: DecLit) to (incl, hi: DecLit) => makeRange(scrut, lo, hi, incl, inner) + case (lo: Literal) to (_, hi: Literal) => + raise(ErrorReport(msg"Incompatible range types: ${lo.describe} to ${hi.describe}" -> pat.toLoc :: Nil)) + failure + case lit: StrLit => B(scrut(), P.Lit(lit), inner(Map.empty)) ~: Sp.End + case App(Ident("~"), Tup(prefix :: postfix :: Nil)) => + stringPrefix(scrut, prefix, (captures1, postfixScrut) => + full(postfixScrut, postfix, captures2 => inner(captures2 ++ captures1))) + case ctor @ (_: Ident | _: Sel) => + val clsTrm = elaborator.cls(ctor) + clsTrm.symbol.flatMap(_.asClsLike) match + case S(cls: (ClassSymbol | ModuleSymbol)) => + B(scrut(), P.ClassLike(cls, clsTrm, N, false)(ctor), inner(Map.empty)) ~: Sp.End + case S(psym: PatternSymbol) => + makeUnapplyBranch(scrut(), psym, inner(Map.empty))(Sp.End) + case _ => + // Raise an error and discard `sequel`. Use `fallback` instead. + raise(ErrorReport(msg"Cannot use this ${ctor.describe} as an extractor" -> ctor.toLoc :: Nil)) + failure + case _ => + raise(ErrorReport(msg"Unrecognized pattern." -> pat.toLoc :: Nil)) + Sp.Else(Term.Error) + + /** Generate a split that consumes the prefix of the scrutinee. */ + private def stringPrefix(scrut: Scrut, pat: Tree, inner: PostfixInner): Sp = trace( + pre = s"stringPrefix <<< $pat", + post = (split: Sp) => s"stringPrefix >>> $split" + ): + pat match + case lhs or rhs => stringPrefix(scrut, lhs, inner) ~~: stringPrefix(scrut, rhs, inner) + case (lo: StrLit) to (incl, hi: StrLit) => + // String range bounds must be single characters. + val ds = Buffer.empty[(Message, Option[Loc])] + if lo.value.length != 1 then + ds += msg"String range bounds must have only one character." -> lo.toLoc + if hi.value.length != 1 then + ds += msg"String range bounds must have only one character." -> hi.toLoc + if ds.nonEmpty then + error(ds.toSeq*) + failure + else + val headRes = FlowSymbol("head") + val headTerm = callStringGet(scrut(), 0, headRes) + val tailRes = FlowSymbol("tail") + val tailTerm = callStringDrop(scrut(), 1, tailRes) + tempLet(headTerm): headSym => + tempLet(tailTerm): tailSym => + makeRange(() => headSym.ref(), lo, hi, incl, captures => + inner(Map.empty, () => tailSym.ref())) + case lit: StrLit => + val checkRes = FlowSymbol("startsWith") + val checkPrefix = callStringStartsWith(scrut(), Term.Lit(lit), checkRes) + plainTest(checkPrefix): + val slicedRes = FlowSymbol("sliced") + val slicedTerm = callStringDrop(scrut(), lit.value.length, slicedRes) + tempLet(slicedTerm): slicedSym => + inner(Map.empty, () => slicedSym.ref()) + case App(Ident("~"), Tup(prefix :: postfix :: Nil)) => + stringPrefix(scrut, prefix, (captures1, postfixScrut1) => + stringPrefix(postfixScrut1, postfix, (captures2, postfixScrut2) => + inner(captures2 ++ captures1, postfixScrut2))) + case ctor @ (_: Ident | _: Sel) => + val clsTrm = elaborator.cls(ctor) + clsTrm.symbol.flatMap(_.asClsLike) match + case S(cls: (ClassSymbol | ModuleSymbol)) => + raise(ErrorReport(msg"Cannot match ${ctor.describe} as a string prefix" -> ctor.toLoc :: Nil)) + failure + case S(psym: PatternSymbol) => + makeUnapplyStringPrefixBranch(scrut(), psym, postfixSym => + inner(Map.empty, () => postfixSym.ref()) + )(Sp.End) + case _ => + raise(ErrorReport(msg"Cannot use this ${ctor.describe} as an extractor" -> ctor.toLoc :: Nil)) + failure + case _ => + Sp.End + + /** Create a function that compiles the resulting term of each case. It checks + * the captured references and sort them in the order of parameters. + */ + private def success(params: Ls[Param]): Inner = + val paramIndexMap = params.zipWithIndex.toMap + captures => trace( + pre = s"success <<< ${params.iterator.map(_.sym).mkString(", ")}", + post = (split: Sp) => s"success >>> ${display(split)}" + ): + require(captures.forall(_._1 |> paramIndexMap.contains)) + if captures.size != params.size then + // TODO: report uncaptured parameters + error(msg"Unmatched number of captures and parameters." -> N) + Split.Else(Term.Error) + else + val fields = captures.toList.sortBy(_._1 |> paramIndexMap).map: + case (_, ref) => Fld(FldFlags.empty, ref, N) + Split.Else(matchResult(Term.Tup(fields)(Tup(Nil)))) + + /* The successful matching result used in prefix matching functions. */ + private def prefixSuccess(params: Ls[Param]): PostfixInner = + val paramIndexMap = params.zipWithIndex.toMap + (captures, postfixScrut) => trace( + pre = s"prefixSuccess <<< ${params.iterator.map(_.sym).mkString(", ")}", + post = (split: Sp) => s"prefixSuccess >>> ${display(split)}" + ): + require(captures.forall(_._1 |> paramIndexMap.contains)) + if captures.size != params.size then + // TODO: report uncaptured parameters + error(msg"Unmatched number of captures and parameters." -> N) + Split.Else(Term.Error) + else + val fields = captures.toList.sortBy(_._1 |> paramIndexMap).map: + case (_, ref) => Fld(FldFlags.empty, ref, N) + val head = Fld(FldFlags.empty, postfixScrut(), N) + Split.Else(matchResult(Term.Tup(head :: fields)(Tup(Nil)))) + + /** Failed matctching result. */ + private def failure: Split = Split.Else(matchFailure()) + + /** Make a matcher function that takes a single scrutinee parameter. */ + protected def makeMatcher(name: Str, scrut: TermSymbol, topmost: Split): TermDefinition = + val normalize = new Normalization(tl) + val sym = BlockMemberSymbol(name, Nil) + val ps = ParamList(ParamListFlags(false), Param(FldFlags.empty, scrut, N) :: Nil) + val body = Term.IfLike(Keyword.`if`, topmost)(normalize(topmost)) + val res = FlowSymbol(s"result of $name") + TermDefinition(N, Fun, sym, ps :: Nil, N, S(body), res, TermDefFlags.empty) + + /** Compile a list of matching functions. */ + def apply(params: Ls[Param], body: Tree): Ctxl[Ls[TermDefinition]] = trace( + pre = s"Translator <<< ${params.mkString(", ")} $body", + post = (blk: Ls[TermDefinition]) => s"Translator >>> $blk" + ): + val unapply = + val scrutId = Ident("scrut"): Ident + val scrutSym = TermSymbol(ParamBind, N, scrutId) + val topmost = full(() => scrutSym.ref(scrutId), body, success(params)) ~~: failure + log(s"Compiled `unapply`: ${display(topmost)}") + makeMatcher("unapply", scrutSym, topmost) + val unapplyStringPrefix = + val scrutId = Ident("topic"): Ident + val scrutSym = TermSymbol(ParamBind, N, scrutId) + val topmost = stringPrefix(() => scrutSym.ref(), body, prefixSuccess(params)) ~~: failure + if topmost is Sp.End then + N + else + log(s"Compiled `unapplyStringPrefix`: ${display(topmost)}") + S(makeMatcher("unapplyStringPrefix", scrutSym, topmost)) + unapply :: unapplyStringPrefix.toList diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Keyword.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Keyword.scala index 69e03eee8..5c1e680a2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Keyword.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Keyword.scala @@ -113,6 +113,7 @@ object Keyword: val `throw` = Keyword("throw", N, curPrec) val `import` = Keyword("import", N, curPrec) val `this` = Keyword("this", N, N) + val `pattern` = Keyword("pattern", N, N) // * The lambda operator is special: // * it should associate very strongly on the left and very loosely on the right diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala index 7048f3fc8..6e845e6eb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala @@ -272,6 +272,20 @@ class ParseRules(using State): End(N), ) ) { (lhs, rhs) => TypeDef(Als, lhs, rhs, N) }, + Kw(`pattern`): + ParseRule("pattern declaration"): + Expr( + ParseRule("pattern head")( + Kw(`=`): + ParseRule("pattern declaration equals sign"): + Expr( + ParseRule("pattern declaration right-hand side")( + End(()) + ) + ) { case (rhs, ()) => S(rhs) }, + End(N), + ) + ) { (lhs, rhs) => TypeDef(Pat, lhs, rhs, N) }, Kw(`class`)(typeDeclBody(Cls)), Kw(`trait`)(typeDeclBody(Trt)), Kw(`module`)(typeDeclBody(Mod)), diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala index 13dda8664..46c4bb671 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala @@ -209,6 +209,7 @@ case object Mxn extends TypeDefKind("mixin") case object Als extends TypeDefKind("type alias") case object Mod extends TypeDefKind("module") with ClsLikeKind case object Obj extends TypeDefKind("object") with ClsLikeKind +case object Pat extends TypeDefKind("pattern") with ClsLikeKind @@ -283,6 +284,7 @@ trait TypeDefImpl(using semantics.Elaborator.State) extends TypeOrTermDef: case Cls => semantics.ClassSymbol(this, name.getOrElse(Ident(""))) case Mod | Obj => semantics.ModuleSymbol(this, name.getOrElse(Ident(""))) case Als => semantics.TypeAliasSymbol(name.getOrElse(Ident(""))) + case Pat => semantics.PatternSymbol(name.getOrElse(Ident(""))) case Trt | Mxn => ??? lazy val definedSymbols: Map[Str, semantics.BlockMemberSymbol] = diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs index 27c6c0021..9b7730dbb 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs @@ -1,6 +1,21 @@ const Predef$class = class Predef { constructor() { - + this.MatchResult = function MatchResult(captures1) { return new MatchResult.class(captures1); }; + this.MatchResult.class = class MatchResult { + constructor(captures) { + this.captures = captures; + + } + toString() { return "MatchResult(" + this.captures + ")"; } + }; + this.MatchFailure = function MatchFailure(errors1) { return new MatchFailure.class(errors1); }; + this.MatchFailure.class = class MatchFailure { + constructor(errors) { + this.errors = errors; + + } + toString() { return "MatchFailure(" + this.errors + ")"; } + }; } id(x) { return x; @@ -33,6 +48,15 @@ const Predef$class = class Predef { tupleGet(xs1, i1) { return globalThis.Array.prototype.at.call(xs1, i1); } + stringStartsWith(string, prefix) { + return string.startsWith(prefix); + } + stringGet(string1, i2) { + return string1.at(i2); + } + stringDrop(string2, n) { + return string2.slice(n); + } checkArgs(functionName, expected, got) { let scrut, name, scrut1, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6; scrut = got != expected; diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mls b/hkmc2/shared/src/test/mlscript-compile/Predef.mls index f56a9ceaa..f54a62075 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mls @@ -21,6 +21,15 @@ fun tupleSlice(xs, i, j) = fun tupleGet(xs, i) = globalThis.Array.prototype.at.call(xs, i) +fun stringStartsWith(string, prefix) = string.startsWith(prefix) + +fun stringGet(string, i) = string.at(i) + +fun stringDrop(string, n) = string.slice(n) + +class MatchResult(captures) +class MatchFailure(errors) + fun checkArgs(functionName, expected, got) = if got != expected then let name = if functionName.length > 0 then " '" + functionName + "'" else "" diff --git a/hkmc2/shared/src/test/mlscript/rp/EmptyString.mls b/hkmc2/shared/src/test/mlscript/rp/EmptyString.mls new file mode 100644 index 000000000..a94c40969 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/EmptyString.mls @@ -0,0 +1,21 @@ +:js + +pattern Oops = "" ~ (Oops | "") + +:re +"" is Oops +//│ ═══[RUNTIME ERROR] RangeError: Maximum call stack size exceeded + + +pattern Funny = "" ~ "" ~ "" + +"" is Funny +//│ = true + +pattern Funny = "" ~ "hello" ~ "" + +"hello" is Funny +//│ = true + +"" is Funny +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/rp/Future.mls b/hkmc2/shared/src/test/mlscript/rp/Future.mls new file mode 100644 index 000000000..9a4e3e64b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/Future.mls @@ -0,0 +1,56 @@ +:js + +:todo // Parameterized patterns. +pattern Rep0[A] = "" | A ~ Rep0[A] +//│ ╔══[ERROR] Cannot use this identifier as an extractor +//│ ║ l.4: pattern Rep0[A] = "" | A ~ Rep0[A] +//│ ╙── ^ +//│ ╔══[ERROR] Cannot use this identifier as an extractor +//│ ║ l.4: pattern Rep0[A] = "" | A ~ Rep0[A] +//│ ╙── ^ + +:todo +pattern Rep0(pattern A, B, C)(head) = + "" | (A as head) ~ Rep0[A] +//│ ╔══[ERROR] Multiple parameter lists are not supported for this definition. +//│ ║ l.13: pattern Rep0(pattern A, B, C)(head) = +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.14: "" | (A as head) ~ Rep0[A] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ /!!!\ Uncaught error: scala.MatchError: TypeDef(Pat,Ident(A),None,None) (of class hkmc2.syntax.Tree$TypeDef) + +:todo // Pattern extractions via aliases. +pattern Email(name, domain) = + (Identifier as name) ~ "@" ~ (Identifier as domain) +//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed + +:todo // View patterns +pattern GreaterThan(value) = case + n and n > value then n +//│ ╔══[ERROR] Unrecognized pattern. +//│ ║ l.29: n and n > value then n +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ +//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed + +:todo +// Normal view pattern +fun view = case + n and n > 5 then n + else undefined +fun foo(x) = if x is + view as + Unit then .... + Arrow(...) then .... +//│ ╔══[ERROR] Unrecognized pattern split. +//│ ║ l.41: view as +//│ ║ ^^^^^^^ +//│ ║ l.42: Unit then .... +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ║ l.43: Arrow(...) then .... +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ +//│ /!!!\ Uncaught error: scala.NotImplementedError: getters (of class String) + + +// In the future, any function can be a pattern +// (x => view(x)) as ... +// (case { ... }) as ... diff --git a/hkmc2/shared/src/test/mlscript/rp/JoinPatterns.mls b/hkmc2/shared/src/test/mlscript/rp/JoinPatterns.mls new file mode 100644 index 000000000..8d7164875 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/JoinPatterns.mls @@ -0,0 +1,32 @@ +:js + +pattern SimpleJoin = ("abc" | "xyz") ~ ("def" | "") + +"abc" is SimpleJoin +//│ = true + +"abcdef" is SimpleJoin +//│ = true + +"xyzdef" is SimpleJoin +//│ = true + +"xyz" is SimpleJoin +//│ = true + +"abcxyzdef" is SimpleJoin +//│ = false + +:fixme +pattern Exponential = ("a" | "b") ~ ("c" | "d") ~ ("e" | "f") ~ ("g" | "h") ~ ("i" | "j") ~ ("k" | "l") ~ ("m" | "n") ~ ("o" | "p") ~ ("q" | "r") ~ ("s" | "t") ~ ("u" | "v") ~ ("w" | "x") ~ ("y" | "z") +//│ /!!!\ Uncaught error: java.lang.StackOverflowError + +:fixme +"abcdefghijklm" is Exponential +//│ ╔══[ERROR] Name not found: Exponential +//│ ║ l.25: "abcdefghijklm" is Exponential +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[ERROR] Cannot use this identifier as a pattern +//│ ║ l.25: "abcdefghijklm" is Exponential +//│ ╙── ^^^^^^^^^^^ +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/rp/MatchResult.mls b/hkmc2/shared/src/test/mlscript/rp/MatchResult.mls new file mode 100644 index 000000000..69bc66734 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/MatchResult.mls @@ -0,0 +1,13 @@ +:js + +MatchResult(0) +//│ = MatchResult { captures: 0 } + +if MatchResult(0) is MatchResult(x) then x +//│ = 0 + +MatchFailure("oops") +//│ = MatchFailure { errors: 'oops' } + +if MatchFailure("oops") is MatchFailure(x) then x +//│ = 'oops' diff --git a/hkmc2/shared/src/test/mlscript/rp/RangePatterns.mls b/hkmc2/shared/src/test/mlscript/rp/RangePatterns.mls new file mode 100644 index 000000000..9f64e3896 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/RangePatterns.mls @@ -0,0 +1,88 @@ +:js + +pattern Lower = "a"..="z" + +"a" is Lower +//│ = true + +"z" is Lower +//│ = true + +"0" is Lower +//│ = false + +pattern Upper = "A"..="Z" + +"A" is Upper +//│ = true + +"Q" is Upper +//│ = true + +"b" is Upper +//│ = false + +pattern UnsignedShort = 0 ..< 65536 + +0 is UnsignedShort +//│ = true + +-1 is UnsignedShort +//│ = false + +65535 is UnsignedShort +//│ = true + +65536 is UnsignedShort +//│ = false + +2147483647 is UnsignedShort +//│ = false + +"b" is UnsignedShort +//│ = false + +[] is UnsignedShort +//│ = true + +:pe +:e +// Don't forget to add a space between numbers and the range operator. +pattern UnsignedByte = 0..< 256 +//│ ╔══[LEXICAL ERROR] Expect at least one digit after the decimal point +//│ ║ l.51: pattern UnsignedByte = 0..< 256 +//│ ╙── ^ +//│ ╔══[ERROR] Unrecognized pattern. +//│ ║ l.51: pattern UnsignedByte = 0..< 256 +//│ ╙── ^^^^^^^^ + +:e +pattern BadRange = "s"..=0 +//│ ╔══[ERROR] Incompatible range types: string literal to integer literal +//│ ║ l.60: pattern BadRange = "s"..=0 +//│ ╙── ^^^^^^^ + +// It becomes an absurd pattern. +0 is BadRange +//│ = false + +:e +pattern BadRange = 0 ..= "s" +//│ ╔══[ERROR] Incompatible range types: integer literal to string literal +//│ ║ l.70: pattern BadRange = 0 ..= "s" +//│ ╙── ^^^^^^^^^ + +:e +pattern BadRange = "yolo" ..= "swag" +//│ ╔══[ERROR] String range bounds must have only one character. +//│ ║ l.76: pattern BadRange = "yolo" ..= "swag" +//│ ║ ^^^^^^ +//│ ╟── String range bounds must have only one character. +//│ ║ l.76: pattern BadRange = "yolo" ..= "swag" +//│ ╙── ^^^^^^ +//│ ╔══[ERROR] String range bounds must have only one character. +//│ ║ l.76: pattern BadRange = "yolo" ..= "swag" +//│ ║ ^^^^^^ +//│ ╟── String range bounds must have only one character. +//│ ║ l.76: pattern BadRange = "yolo" ..= "swag" +//│ ╙── ^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/rp/Simple.mls b/hkmc2/shared/src/test/mlscript/rp/Simple.mls new file mode 100644 index 000000000..cf3438191 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/Simple.mls @@ -0,0 +1,26 @@ +:js + +pattern Zero = "0" + +"0" is Zero +//│ = true + +pattern ManyZeros = "0" ~ (ManyZeros | "") + +"" is ManyZeros +//│ = false + +"0" is ManyZeros +//│ = true + +"000" is ManyZeros +//│ = true + +"0001" is ManyZeros +//│ = false + +"1" is ManyZeros +//│ = false + +"1000" is ManyZeros +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/rp/examples/Identifier.mls b/hkmc2/shared/src/test/mlscript/rp/examples/Identifier.mls new file mode 100644 index 000000000..3c7c6665d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/examples/Identifier.mls @@ -0,0 +1,115 @@ +:js + +pattern Zero = "0" + +"0" is Zero +//│ = true + +pattern Binary = "0" | "1" + +"2" is Binary +//│ = false + +pattern Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + +"0" is Digit +//│ = true + +"9" is Digit +//│ = true + +"a" is Digit +//│ = false + +pattern Lower = "a"..="z" + +"a" is Lower +//│ = true + +"z" is Lower +//│ = true + +"0" is Lower +//│ = false + +pattern Upper = "A"..="Z" + +"A" is Upper +//│ = true + +"Q" is Upper +//│ = true + +"b" is Upper +//│ = false + +pattern Letter = Lower | Upper + +"b" is Letter +//│ = true + +"V" is Letter +//│ = true + +"0" is Letter +//│ = false + +"9" is Letter +//│ = false + +pattern Word = Letter ~ (Word | "") + +"" is Word +//│ = false + +"b" is Word +//│ = true + +"pattern" is Word +//│ = true + +"b0rked" is Word +//│ = false + +pattern ManyDigits = ("0" ..= "9") ~ (ManyDigits | "") + +"0" is ManyDigits +//│ = true + +"42" is ManyDigits +//│ = true + +"1234" is ManyDigits +//│ = true + +pattern Integer = "0" | ("1" ..= "9") ~ (ManyDigits | "") + +"0" is Integer +//│ = true + +"012" is Integer +//│ = false + +"42" is Integer +//│ = true + +pattern IdentifierStart = Letter | "_" + +pattern IdentifierBody = (Letter | Digit | "_") ~ (IdentifierBody | "") + +pattern Identifier = IdentifierStart ~ (IdentifierBody | "") + +"abc" is Identifier +//│ = true + +"abc123" is Identifier +//│ = true + +"abc_123" is Identifier +//│ = true + +"_abc_123" is Identifier +//│ = true + +"123abc" is Identifier +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/rp/examples/Number.mls b/hkmc2/shared/src/test/mlscript/rp/examples/Number.mls new file mode 100644 index 000000000..603d9342c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/examples/Number.mls @@ -0,0 +1,174 @@ +:js + +pattern Digits = ("0"..="9") ~ (Digits | "") + +pattern Integer = "0" | ("1"..="9") ~ (Digits | "") + +pattern FractionalPart = "." ~ Digits + +".1" is FractionalPart +//│ = true + +pattern ExponentPart = ("e" | "E") ~ ("+" | "-" | "") ~ Integer + +"e10" is ExponentPart +//│ = true + +"e100" is ExponentPart +//│ = true + +pattern Number = Integer ~ (FractionalPart | "") ~ (ExponentPart | "") + +// Test cases for Digits pattern +// ============================= +:expect true +"123" is Digits +//│ = true + +:expect true +"0" is Digits +//│ = true + +:expect true +"9" is Digits +//│ = true + +:expect false +"a" is Digits +//│ = false + + +// Test cases for Integer pattern +// ============================== +:expect true +"0" is Integer +//│ = true + +:expect true +"123" is Integer +//│ = true + +:expect false +"001" is Integer +//│ = false + +:expect false +"a" is Integer +//│ = false + + +// Test cases for FractionalPart pattern +// ===================================== +:expect true +".123" is FractionalPart +//│ = true + +:expect true +".0" is FractionalPart +//│ = true + +:expect false +"." is FractionalPart +//│ = false + +:expect false +"0.1" is FractionalPart +//│ = false + + +// Test cases for ExponentPart pattern +// =================================== +:expect true +"e10" is ExponentPart +//│ = true + +:expect true +"E-10" is ExponentPart +//│ = true + +:expect true +"e+10" is ExponentPart +//│ = true + +:expect false +"e" is ExponentPart +//│ = false + +:expect false +"e1a" is ExponentPart +//│ = false + + +// Test cases for Number pattern +// ============================= +:expect true +"3.14" is Number +//│ = true + +:expect true +"42" is Number +//│ = true + +:expect true +"3.14e10" is Number +//│ = true + +:expect true +"1e100" is Number +//│ = true + +:expect true +"1234e-789" is Number +//│ = true + +:expect true +"0.0314E+2" is Number +//│ = true + +:expect true +"0.0314E-2" is Number +//│ = true + +:expect false +"." is Number +//│ = false + +:expect false +"e10" is Number +//│ = false + +:expect false +"3.14e" is Number +//│ = false + +:expect true +"3.14" is Number +//│ = true + +:expect true +"42" is Number +//│ = true + +:expect true +"3.14e10" is Number +//│ = true + +:expect true +"1e100" is Number +//│ = true + +:expect true +"1234e-789" is Number +//│ = true + +:expect true +"0.0314E+2" is Number +//│ = true + +:expect true +"0.0314E-2" is Number +//│ = true + +:expect true +"1.7976931348623158e+308" is Number +//│ = true