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 6303d9c63..a939a91be 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, Elaborator.Ctx) 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 @@ -191,7 +192,7 @@ class JSBuilder(using Elaborator.State, Elaborator.Ctx) 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 d13bdc8ff..b3cc92ad5 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(SynthSel(SynthSel(Ident("globalThis"), Ident("Predef")), Ident("tupleSlice"))) - - private lazy val tupleGet = - term(SynthSel(SynthSel(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, clsTrm, 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)) @@ -411,12 +407,6 @@ class Desugarer(tl: TraceLogger, elaborator: Elaborator) case ((lead, N), Spread(_, _, patOpt)) => (lead, S((patOpt, Nil))) 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 val (wrapRest, restMatches) = rest match @@ -426,7 +416,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 2146cca04..d612056e4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -108,6 +108,12 @@ object Elaborator: given State = this val suid = new Uid.Symbol.State val globalThisSymbol = TopLevelSymbol("globalThis") + val builtinOpsMap = + val baseBuiltins = binaryOps.map: op => + op -> BuiltinSymbol(op, binary = true, unary = unaryOps(op), nullary = false) + .toMap + baseBuiltins ++ aliasOps.map: + case (alias, base) => alias -> baseBuiltins(base) val seqSymbol = TermSymbol(ImmutVal, N, Ident(";")) def init(using State): Ctx = Ctx.empty.copy(env = Map( "globalThis" -> globalThisSymbol, @@ -131,13 +137,6 @@ extends Importer: private val allocSkolemSym = VarSymbol(Ident("Alloc")) private val allocSkolemDef = TyParam(FldFlags.empty, N, allocSkolemSym) allocSkolemSym.decl = S(allocSkolemDef) - - private val builtinOpsMap = - val baseBuiltins = binaryOps.map: op => - op -> BuiltinSymbol(op, binary = true, unary = unaryOps(op), nullary = false) - .toMap - baseBuiltins ++ aliasOps.map: - case (alias, base) => alias -> baseBuiltins(base) def mkLetBinding(sym: LocalSymbol, rhs: Term): Ls[Statement] = LetDecl(sym) :: DefineVar(sym, rhs) :: Nil @@ -226,7 +225,7 @@ extends Importer: ctx.get(name) match case S(sym) => sym.ref(id) case N => - builtinOpsMap.get(name) match + state.builtinOpsMap.get(name) match case S(bi) => bi.ref(id) case N => raise(ErrorReport(msg"Name not found: $name" -> tree.toLoc :: Nil)) @@ -651,7 +650,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) => @@ -703,6 +702,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 translate = new ucs.Translator(this) + val bod = translate(ps.map(_.params).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 7ced4c21c..a5de8e41f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -28,21 +28,23 @@ abstract class Symbol(using State) extends Located: def asCls: Opt[ClassSymbol] = this match case cls: ClassSymbol => S(cls) - case mem: BlockMemberSymbol => - mem.clsTree.map(_.symbol.asInstanceOf[ClassSymbol]) + case mem: BlockMemberSymbol => mem.clsTree.flatMap(_.symbol.asCls) case _ => N def asMod: Opt[ModuleSymbol] = this match case cls: ModuleSymbol => S(cls) - case mem: BlockMemberSymbol => - mem.modTree.map(_.symbol.asInstanceOf[ModuleSymbol]) + case mem: BlockMemberSymbol => mem.modTree.flatMap(_.symbol.asMod) + case _ => N + def asPat: Opt[PatternSymbol] = this match + case pat: PatternSymbol => S(pat) + case mem: BlockMemberSymbol => mem.patTree.flatMap(_.symbol.asPat) case _ => N def asAls: Opt[TypeAliasSymbol] = this match case cls: TypeAliasSymbol => S(cls) - case mem: BlockMemberSymbol => - mem.alsTree.map(_.symbol.asInstanceOf[TypeAliasSymbol]) + case mem: BlockMemberSymbol => mem.alsTree.flatMap(_.symbol.asAls) 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 @@ -101,6 +103,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: @@ -174,6 +178,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}${State.dbgUid(uid)}" +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 fb7b19dfe..5a67992e4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -120,6 +120,8 @@ sealed trait Statement extends AutoLocated with ProductWithExtraInfo: mod.paramsOpt.toList.flatMap(_.subTerms) ::: mod.body.blk :: Nil case td: TypeDef => td.rhs.toList + case pat: PatternDef => + pat.paramsOpt.toList.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 @@ -256,6 +258,16 @@ case class ModuleDef( body: ObjBody, ) extends ClassLikeDef with Companion +case class PatternDef( + owner: Opt[InnerSymbol], + sym: PatternSymbol, + tparams: Ls[TyParam], + paramsOpt: Opt[ParamList], + 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..bfb92855a --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/DesugaringBase.scala @@ -0,0 +1,114 @@ +package hkmc2 +package semantics +package ucs + +import mlscript.utils.*, shorthands.* +import syntax.Tree.*, Elaborator.{Ctxl, ctx} + +/** Contains some helpers that makes UCS desugaring easier. */ +trait DesugaringBase(using state: Elaborator.State): + val elaborator: Elaborator + + import elaborator.tl.*, state.globalThisSymbol + + protected final def sel(p: Term, k: Ident): Term.SynthSel = Term.SynthSel(p, k)(N) + protected final def sel(p: Term, k: Ident, s: FieldSymbol): Term.SynthSel = Term.SynthSel(p, k)(S(s)) + protected final def sel(p: Term, k: Str): Term.SynthSel = sel(p, Ident(k): Ident) + protected final def sel(p: Term, k: Str, s: FieldSymbol): Term.SynthSel = sel(p, Ident(k): Ident, s) + protected final def int(i: Int) = Term.Lit(IntLit(BigInt(i))) + protected final def str(s: Str) = Term.Lit(StrLit(s)) + protected final def fld(t: Term) = Fld(FldFlags.empty, t, N) + protected final def tup(xs: Fld*): Term.Tup = Term.Tup(xs.toList)(Tup(Nil)) + protected final def app(l: Term, r: Term, label: Str): Term.App = app(l, r, FlowSymbol(label)) + protected final def app(l: Term, r: Term, s: FlowSymbol): Term.App = Term.App(l, r)(App(Empty(), Empty()), s) + + /** Get the class symbol defined in the `Predef` module. */ + protected def resolvePredefMember(name: Str): Ctxl[(Term.SynthSel, ClassSymbol)] = + val predefSymbol = ctx.Builtins.Predef + val innerSel = sel(globalThisSymbol.ref(), "Predef", predefSymbol) + val memberSymbol = predefSymbol.tree.definedSymbols.get(name).flatMap(_.asCls).getOrElse: + lastWords(s"Cannot resolve `$name` in `Predef`.") + (sel(innerSel, name, memberSymbol), memberSymbol) + + /** Make a term looks like `globalThis.Predef.MatchResult` with its symbol. */ + protected lazy val matchResultClass: Ctxl[(Term.SynthSel, ClassSymbol)] = resolvePredefMember("MatchResult") + + /** Make a pattern looks like `globalThis.Predef.MatchResult.class`. */ + protected def matchResultPattern(parameters: Opt[List[BlockLocalSymbol]]): Ctxl[Pattern.ClassLike] = + val (classRef, classSym) = matchResultClass + val classSel = Term.SynthSel(matchResultClass._1, Ident("class"))(S(classSym)) + Pattern.ClassLike(classSym, classSel, parameters, false)(Empty()) + + /** Make a term looks like `globalThis.Predef.MatchFailure` with its symbol. */ + protected lazy val matchFailureClass: Ctxl[(Term.SynthSel, ClassSymbol)] = resolvePredefMember("MatchFailure") + + /** Make a pattern looks like `globalThis.Predef.MatchFailure.class`. */ + protected def matchFailurePattern(parameters: Opt[List[BlockLocalSymbol]]): Ctxl[Pattern.ClassLike] = + val (classRef, classSym) = matchResultClass + val classSel = Term.SynthSel(matchResultClass._1, Ident("class"))(S(classSym)) + Pattern.ClassLike(classSym, classSel, parameters, false)(Empty()) + + /** Create a term that selects a method in the `Predef` module. */ + protected final def selectPredefMethod = + sel(sel(globalThisSymbol.ref(), "Predef"), _: Str) + + protected lazy val tupleSlice = selectPredefMethod("tupleSlice") + protected lazy val tupleGet = selectPredefMethod("tupleGet") + protected lazy val stringStartsWith = selectPredefMethod("stringStartsWith") + protected lazy val stringGet = selectPredefMethod("stringGet") + protected lazy val stringDrop = selectPredefMethod("stringDrop") + + /** Make a term that looks like `tupleGet(t, i)`. */ + protected final def callTupleGet(t: Term, i: Int, label: Str): Ctxl[Term] = + callTupleGet(t, i, FlowSymbol(label)) + + /** Make a term that looks like `tupleGet(t, i)`. */ + protected final def callTupleGet(t: Term, i: Int, s: FlowSymbol): Ctxl[Term] = + app(tupleGet, tup(fld(t), fld(int(i))), s) + + /** Make a term that looks like `stringStartsWith(t, p)`. */ + protected final def callStringStartsWith(t: Term, p: Term, label: Str): Ctxl[Term] = + app(stringStartsWith, tup(fld(t), fld(p)), FlowSymbol(label)) + + /** Make a term that looks like `stringStartsWith(t, i)`. */ + protected final def callStringGet(t: Term, i: Int, label: Str): Ctxl[Term] = + app(stringGet, tup(fld(t), fld(int(i))), FlowSymbol(label)) + + /** Make a term that looks like `stringStartsWith(t, n)`. */ + protected final def callStringDrop(t: Term, n: Int, label: Str): Ctxl[Term] = + app(stringDrop, tup(fld(t), fld(int(n))), FlowSymbol(label)) + + protected final def tempLet(dbgName: Str, term: Term)(inner: TempSymbol => Split): Split = + val s = TempSymbol(N, dbgName) + Split.Let(s, term, inner(s)) + + protected final 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, + clsTerm: Term, + inner: => Split, + method: Str = "unapply" + )(fallback: Split): Ctxl[Split] = + val call = app(sel(clsTerm, method), tup(fld(scrut)), FlowSymbol(s"result of $method")) + tempLet("matchResult", call): resultSymbol => + Branch(resultSymbol.ref(), matchResultPattern(N), inner) ~: fallback + + /** Make a `Branch` that calls `Pattern` symbols' `unapplyStringPrefix` functions. */ + def makeUnapplyStringPrefixBranch( + scrut: => Term.Ref, + clsTerm: Term, + inner: TempSymbol => Split, + method: Str = "unapplyStringPrefix" + )(fallback: Split): Ctxl[Split] = + val call = app(sel(clsTerm, method), tup(fld(scrut)), FlowSymbol(s"result of $method")) + tempLet("matchResult", call): resultSymbol => + val argSym = TempSymbol(N, "arg") + Branch( + resultSymbol.ref(), + matchResultPattern(S(argSym :: Nil)), + tempLet("postfix", callTupleGet(argSym.ref(), 0, "postfix"))(inner) + ) ~: fallback 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..ab8fe1ed0 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Translator.scala @@ -0,0 +1,247 @@ +package hkmc2 +package semantics +package ucs + +import mlscript.utils.*, shorthands.* +import Message.MessageContext +import Split.display, ucs.Normalization +import syntax.{Fun, Keyword, Literal, ParamBind, Tree}, Tree.*, Keyword.`as` +import scala.collection.mutable.{Buffer, Set as MutSet} + +object Translator: + /** A helper extractor for matching the tree of `x | y`. */ + object or: + infix def unapply(tree: Tree): Opt[(Tree, Tree)] = tree match + case App(Ident("|"), Tup(lhs :: rhs :: Nil)) => S(lhs, rhs) + case _ => N + + /** A helper extractor for matching the tree of `x ..= y` and `x ..< y`. + * The Boolean value indicates whether the range is inclusive. + */ + 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 + + private def error(msgs: (Message, Option[Loc])*)(using Raise): Unit = + raise(ErrorReport(msgs.toList)) + + private def warn(msgs: (Message, Option[Loc])*)(using Raise): Unit = + raise(WarningReport(msgs.toList)) + +/** This class translates a tree describing a pattern into functions that can + * perform pattern matching on terms described by the pattern. + */ +class Translator(val elaborator: Elaborator) + (using state: Elaborator.State, c: Elaborator.Ctx) extends DesugaringBase: + import elaborator.tl.*, Translator.* + + extension (split: Split) + private def ~~:(fallback: Split): Split = + if fallback == Split.End || 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) + + /** Each scrutinee is represented by a function that creates a reference to + * the scrutinee symbol. It is sufficient for current implementation. + */ + private type Scrut = () => Term.Ref + + private type CaptureMap = Map[Param, Term.Ref] + + private type Inner = CaptureMap => Split + + private type PrefixInner = (CaptureMap, Scrut) => Split + + private def matchResult(captures: Term) = + app(matchResultClass._1, tup(fld(captures)), FlowSymbol("result of `MatchResult`")) + + private def matchFailure() = + app(matchFailureClass._1, tup(), FlowSymbol("result of `MatchFailure`")) + + private lazy val lteq = state.builtinOpsMap("<=") + private lazy val lt = state.builtinOpsMap("<") + private lazy val eq = state.builtinOpsMap("==") + + private def makeRange(scrut: Scrut, lo: Literal, hi: Literal, rightInclusive: Bool, inner: Inner) = + def scrutFld = fld(scrut()) + val test1 = app(lteq.ref(), tup(fld(Term.Lit(lo)), scrutFld), "gtLo") + val upperOp = if rightInclusive then lteq else lt + val test2 = app(upperOp.ref(), tup(scrutFld, fld(Term.Lit(hi))), "ltHi") + plainTest(test1, "gtLo")(plainTest(test2, "ltHi")(inner(Map.empty))) + + /** String range bounds must be single characters. */ + private def isInvalidStringBounds(lo: StrLit, hi: StrLit)(using Raise): Bool = + 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*) + ds.nonEmpty + + /** Generate a split that consumes the entire scrutinee. */ + private def full(scrut: Scrut, pat: Tree, inner: Inner)(using Raise): Split = trace( + pre = s"full <<< $pat", + post = (split: Split) => s"full >>> $split" + ): + pat match + case lhs or rhs => full(scrut, lhs, inner) ~~: full(scrut, rhs, inner) + case (lo: StrLit) to (incl, hi: StrLit) => if isInvalidStringBounds(lo, hi) then 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) => + error(msg"Incompatible range types: ${lo.describe} to ${hi.describe}" -> pat.toLoc) + failure + case lit: StrLit => Branch(scrut(), Pattern.Lit(lit), inner(Map.empty)) ~: Split.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, inAppPrefix = false) + clsTrm.symbol.flatMap(_.asClsLike) match + case S(cls: (ClassSymbol | ModuleSymbol)) => + Branch(scrut(), Pattern.ClassLike(cls, clsTrm, N, false)(ctor), inner(Map.empty)) ~: Split.End + case S(psym: PatternSymbol) => + makeUnapplyBranch(scrut(), clsTrm, inner(Map.empty))(Split.End) + case _ => + error(msg"Cannot use this ${ctor.describe} as an extractor" -> ctor.toLoc) + errorSplit + case _ => + error(msg"Unrecognized pattern." -> pat.toLoc) + errorSplit + + /** Generate a split that consumes the prefix of the scrutinee. */ + private def stringPrefix(scrut: Scrut, pat: Tree, inner: PrefixInner)(using Raise): Split = trace( + pre = s"stringPrefix <<< $pat", + post = (split: Split) => s"stringPrefix >>> $split" + ): + pat match + case lhs or rhs => stringPrefix(scrut, lhs, inner) ~~: stringPrefix(scrut, rhs, inner) + case (lo: StrLit) to (incl, hi: StrLit) => if isInvalidStringBounds(lo, hi) then failure else + val emptyTest = app(eq.ref(), tup(fld(scrut()), fld(str(""))), "test empty") + val headTerm = callStringGet(scrut(), 0, "head") + val tailTerm = callStringDrop(scrut(), 1, "tail") + plainTest(emptyTest, "emptyTest")(failure) ~~: + tempLet("head", headTerm): headSym => + tempLet("tail", tailTerm): tailSym => + makeRange(() => headSym.ref(), lo, hi, incl, captures => + inner(Map.empty, () => tailSym.ref())) + case (lo: IntLit) to (incl, hi: IntLit) => Split.End + case (lo: DecLit) to (incl, hi: DecLit) => Split.End + case (lo: Literal) to (_, hi: Literal) => + error(msg"Incompatible range types: ${lo.describe} to ${hi.describe}" -> pat.toLoc) + errorSplit + case lit @ StrLit(value) => + plainTest(callStringStartsWith(scrut(), Term.Lit(lit), "startsWith")): + tempLet("sliced", callStringDrop(scrut(), value.length, "sliced")): 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, inAppPrefix = false) + clsTrm.symbol.flatMap(_.asClsLike) match + case S(cls: (ClassSymbol | ModuleSymbol)) => + val kind = cls match { case _: ClassSymbol => "class" case _ => "module" } + error(msg"Cannot treat this $kind as a string prefix" -> ctor.toLoc) + errorSplit + case S(psym: PatternSymbol) => + makeUnapplyStringPrefixBranch(scrut(), clsTrm, postfixSym => + inner(Map.empty, () => postfixSym.ref()) + )(Split.End) + case _ => + error(msg"Cannot use this ${ctor.describe} as an extractor" -> ctor.toLoc) + errorSplit + case _ => + error(msg"Unrecognized pattern." -> pat.toLoc) + errorSplit + + /** 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])(using Raise): Inner = + val paramIndexMap = params.zipWithIndex.toMap + captures => trace( + pre = s"success <<< ${params.iterator.map(_.sym).mkString(", ")}", + post = (split: Split) => s"success >>> ${display(split)}" + ): + require(captures.forall(_._1 |> paramIndexMap.contains)) + if captures.size != params.size then + // TODO: report uncaptured parameters and add tests after captures/extraction is done + 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])(using Raise): PrefixInner = + val paramIndexMap = params.zipWithIndex.toMap + (captures, postfixScrut) => trace( + pre = s"prefixSuccess <<< ${params.iterator.map(_.sym).mkString(", ")}", + post = (split: Split) => 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()) + + private def errorSplit: Split = Split.Else(Term.Error) + + /** Create a function definition from the given UCS splits. */ + private def makeMatcher(name: Str, scrut: TermSymbol, topmost: Split)(using Raise): TermDefinition = + val normalize = new Normalization(elaborator.tl) + val sym = BlockMemberSymbol(name, Nil) + val ps = PlainParamList(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) + + /** Translate a list of extractor/matching functions for the given pattern. + * There are currently two functions: `unapply` and `unapplyStringPrefix`. + * + * - `unapply` is used for matching the entire scrutinee. It returns the + * captured/extracted values. + * - `unapplyStringPrefix` is used for matching the string prefix of the + * scrutinee. It returns the remaining string and the captured/extracted + * values. If the given tree does not represent a string pattern, this + * function will not be generated. + */ + def apply(params: Ls[Param], body: Tree)(using Raise): Ls[TermDefinition] = trace( + pre = s"Translator <<< ${params.mkString(", ")} $body", + post = (blk: Ls[TermDefinition]) => s"Translator >>> $blk" + ): + val unapply = + val scrutSym = TermSymbol(ParamBind, N, Ident("scrut")) + val topmost = full(() => scrutSym.ref(), body, success(params)) ~~: failure + log(s"Translated `unapply`: ${display(topmost)}") + makeMatcher("unapply", scrutSym, topmost) + val unapplyStringPrefix = + // We don't report errors here because they are already reported in the + // translation of `unapply` function. + given Raise = Function.const(()) + val scrutSym = TermSymbol(ParamBind, N, Ident("topic")) + stringPrefix(() => scrutSym.ref(), body, prefixSuccess(params)) match + case Split.End => N + case split => + val topmost = split ~~: failure + log(s"Translated `unapplyStringPrefix`: ${display(topmost)}") + S(makeMatcher("unapplyStringPrefix", scrutSym, topmost ~~: failure)) + 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..8ce2ae86c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala @@ -191,6 +191,22 @@ class ParseRules(using State): ) { case (body, _) => IfLike(kw, body) } ) + def typeAliasLike(kw: Keyword, kind: TypeDefKind): Kw[TypeDef] = + Kw(kw): + ParseRule(s"${kind.desc} declaration"): + Expr( + ParseRule(s"${kind.desc} head")( + Kw(`=`): + ParseRule(s"${kind.desc} declaration equals sign"): + Expr( + ParseRule(s"${kind.desc} declaration right-hand side")( + End(()) + ) + ) { case (rhs, ()) => S(rhs) }, + End(N), + ) + ) { (lhs, rhs) => TypeDef(kind, lhs, rhs, N) } + val prefixRules: ParseRule[Tree] = ParseRule("start of statement", omitAltsStr = true)( letLike(`let`), letLike(`set`), @@ -258,20 +274,8 @@ class ParseRules(using State): , Kw(`fun`)(termDefBody(Fun)), Kw(`val`)(termDefBody(ImmutVal)), - Kw(`type`): - ParseRule("type alias declaration"): - Expr( - ParseRule("type alias head")( - Kw(`=`): - ParseRule("type alias declaration equals sign"): - Expr( - ParseRule("type alias declaration right-hand side")( - End(()) - ) - ) { case (rhs, ()) => S(rhs) }, - End(N), - ) - ) { (lhs, rhs) => TypeDef(Als, lhs, rhs, N) }, + typeAliasLike(`type`, Als), + typeAliasLike(`pattern`, Pat), 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 49e406117..8cc4ca76a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala @@ -214,6 +214,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 @@ -288,6 +289,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 41058291b..b6269ea12 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs @@ -1,5 +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 + ")"; } + }; this.Test = class Test { constructor() { this.y = 1; @@ -43,6 +59,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, isUB, got) { let scrut, name, scrut1, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9; tmp = got < expected; diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mls b/hkmc2/shared/src/test/mlscript-compile/Predef.mls index 5bdfd51fa..c3d63e192 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mls @@ -20,6 +20,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, isUB, got) = if got < expected || isUB && got > expected then let name = if functionName.length > 0 then " '" + functionName + "'" else "" diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mjs b/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mjs index 2e2b55cf7..ed6099249 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mjs @@ -15,9 +15,7 @@ class Bool { } class Accounting { constructor() { - this.warnings = [ - - ]; + this.warnings = []; this.Project = function Project(num1) { return new Project.class(num1); }; this.Project.class = class Project { constructor(num) { @@ -59,9 +57,7 @@ class Accounting { } toString() { return "Line(" + this.name + ", " + this.proj + ", " + this.starting_balance + ", " + this.isMatchable + ")"; } }; - this.lines = [ - - ]; + this.lines = []; this.Report = function Report(fileName1) { return new Report.class(fileName1); }; this.Report.class = class Report { constructor(fileName) { diff --git a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls index 7f1fe45bf..4b8658f03 100644 --- a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls +++ b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls @@ -23,6 +23,8 @@ declare val fs declare module Predef with class Test with val x = 0 + class MatchResult(captures) + class MatchFailure(errors) // TODO: rm 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..8b8510543 --- /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] +//│ ╙── ^ + +:todo +pattern Rep0(pattern A, B, C)(head) = + "" | (A as head) ~ Rep0[A] +//│ ╔══[ERROR] Multiple parameter lists are not supported for this definition. +//│ ║ l.10: pattern Rep0(pattern A, B, C)(head) = +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.11: "" | (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) +//│ ╔══[ERROR] Unrecognized pattern. +//│ ║ l.21: (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..2c3b45b8f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/JoinPatterns.mls @@ -0,0 +1,30 @@ +:js + +pattern SimpleJoin = ("abc" | "xyz") ~ ("def" | "") + +:expect true +"abc" is SimpleJoin +//│ = true + +:expect true +"abcdef" is SimpleJoin +//│ = true + +:expect true +"xyzdef" is SimpleJoin +//│ = true + +:expect true +"xyz" is SimpleJoin +//│ = true + +:expect false +"abcxyzdef" is SimpleJoin +//│ = false + +:todo +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") + +:todo +"abcdefghijklm" is Exponential +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/rp/LocalPatterns.mls b/hkmc2/shared/src/test/mlscript/rp/LocalPatterns.mls new file mode 100644 index 000000000..93bff10a1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/LocalPatterns.mls @@ -0,0 +1,74 @@ +:js + +pattern One = "1" + +module Playground with + pattern Zero = "0" + pattern DoubleZero = Zero ~ Zero + pattern ZeroOne = Zero ~ One + +Playground +//│ > Playground { +//│ > Zero: Zero { class: [class Zero] }, +//│ > DoubleZero: DoubleZero { class: [class DoubleZero] }, +//│ > ZeroOne: ZeroOne { class: [class ZeroOne] }, +//│ > class: [class Playground] +//│ = } + +// Pattern defined in a module can be used with qualified name. +// ============================================================ + +Playground.Zero +//│ = Zero { class: [class Zero] } + +Playground.Zero.unapply("0") +//│ = MatchResult { captures: [] } + +:expect true +"0" is Playground.Zero +//│ = true + +// Patterns defined in a module can refer to other patterns in the same module. +// ============================================================================ + +Playground.DoubleZero +//│ = DoubleZero { class: [class DoubleZero] } + +Playground.DoubleZero.unapply("00") +//│ = MatchResult { captures: [] } + +:expect true +"00" is Playground.DoubleZero +//│ = true + +// Patterns defined in a module can refer to patterns in the global scope. +// ======================================================================= + +Playground.ZeroOne +//│ = ZeroOne { class: [class ZeroOne] } + +Playground.ZeroOne.unapply("01") +//│ = MatchResult { captures: [] } + +:expect true +"01" is Playground.ZeroOne +//│ = true + +// Patterns defined in the global scope can refer to patterns in a module. +// ======================================================================= + +pattern TripleZero = Playground.DoubleZero ~ Playground.Zero + +TripleZero +//│ = TripleZero { class: [class TripleZero] } + +TripleZero.unapply("000") +//│ = MatchResult { captures: [] } + +:expect true +"000" is TripleZero +//│ = true + +:expect false +"001" is TripleZero +//│ = 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..4cdc645ae --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/MatchResult.mls @@ -0,0 +1,42 @@ +: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' + +class MatchResult(x, y, z) + +MatchResult(0) +//│ = MatchResult { x: 0, y: undefined, z: undefined } + +pattern Cross = "X" + +Cross.unapply("X") +//│ = MatchResult { captures: [] } + +Cross.unapply("0") +//│ = MatchFailure { errors: undefined } + +:sjs +fun foo(x) = x is Cross +//│ JS: +//│ function foo(...args) { +//│ globalThis.Predef.checkArgs("foo", 1, true, args.length); +//│ let x = args[0]; +//│ let matchResult; +//│ matchResult = globalThis.Cross.unapply(x) ?? null; +//│ if (matchResult instanceof globalThis.Predef.MatchResult.class) { +//│ return true; +//│ } else { +//│ return false; +//│ } +//│ } +//│ undefined 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..016b206b1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/RangePatterns.mls @@ -0,0 +1,82 @@ +: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" +//│ ╙── ^^^^^^ 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..414770b4c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/Simple.mls @@ -0,0 +1,37 @@ +:js + +pattern Zero = "0" + +:expect true +Zero.unapply("0") is MatchResult +//│ = true + +:expect true +"0" is Zero +//│ = true + +pattern ManyZeros = "0" ~ (ManyZeros | "") + +:expect false +"" is ManyZeros +//│ = false + +:expect true +"0" is ManyZeros +//│ = true + +:expect true +"000" is ManyZeros +//│ = true + +:expect false +"0001" is ManyZeros +//│ = false + +:expect false +"1" is ManyZeros +//│ = false + +:expect 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..e0a5863ca --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/rp/examples/Identifier.mls @@ -0,0 +1,145 @@ +:js + +pattern Zero = "0" + +:expect true +"0" is Zero +//│ = true + +pattern Binary = "0" | "1" + +:expect false +"2" is Binary +//│ = false + +pattern Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + +:expect true +"0" is Digit +//│ = true + +:expect true +"9" is Digit +//│ = true + +:expect false +"a" is Digit +//│ = false + +pattern Lower = "a"..="z" + +:expect true +"a" is Lower +//│ = true + +:expect true +"z" is Lower +//│ = true + +:expect false +"0" is Lower +//│ = false + +pattern Upper = "A"..="Z" + +:expect true +"A" is Upper +//│ = true + +:expect true +"Q" is Upper +//│ = true + +:expect false +"b" is Upper +//│ = false + +pattern Letter = Lower | Upper + +:expect true +"b" is Letter +//│ = true + +:expect true +"V" is Letter +//│ = true + +:expect false +"0" is Letter +//│ = false + +:expect false +"9" is Letter +//│ = false + +pattern Word = Letter ~ (Word | "") + +:expect false +"" is Word +//│ = false + +:expect true +"b" is Word +//│ = true + +:expect true +"pattern" is Word +//│ = true + +:expect false +"b0rked" is Word +//│ = false + +pattern ManyDigits = ("0" ..= "9") ~ (ManyDigits | "") + +:expect true +"0" is ManyDigits +//│ = true + +:expect true +"42" is ManyDigits +//│ = true + +:expect true +"1234" is ManyDigits +//│ = true + +pattern Integer = "0" | ("1" ..= "9") ~ (ManyDigits | "") + +:expect true +"0" is Integer +//│ = true + +:expect false +"012" is Integer +//│ = false + +:expect true +"42" is Integer +//│ = true + +pattern IdentifierStart = Letter | "_" + +pattern IdentifierBody = (Letter | Digit | "_") ~ (IdentifierBody | "") + +pattern Identifier = IdentifierStart ~ (IdentifierBody | "") + +:expect true +"abc" is Identifier +//│ = true + +:expect true +"abc123" is Identifier +//│ = true + +:expect true +"abc_123" is Identifier +//│ = true + +:expect true +"_abc_123" is Identifier +//│ = true + +:expect false +"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