diff --git a/compiler/shared/test/diff/Lifter.mls b/compiler/shared/test/diff/Lifter.mls index 93c69284d9..89f80cad87 100644 --- a/compiler/shared/test/diff/Lifter.mls +++ b/compiler/shared/test/diff/Lifter.mls @@ -151,16 +151,16 @@ class A(x: Int): {a1: Int} & B & D(x){ } } //│ |#class| |B|‹|T|›| |{||}|↵|#class| |C| |{||}|↵|#class| |D|(|y|#:| |Int|)| |{||}|↵|#class| |A|‹|T|,| |U|›|(|x|#:| |Int|)|#:| |{|a1|#:| |Int|}| |&| |B|‹|T|›| |&| |D|(|x|)|{|→|#fun| |getA|(||)| |#=| |#new| |C|{|→|#fun| |foo|(|x|#:| |T|)| |#=| |x|←|↵|}|←|↵|}| -//│ Parsed: {class B‹T›() {}; class C() {}; class D(y: Int,) {}; class A‹T, U›(x: Int,): & ('{' {a1: Int} '}',) (& (B‹T›,) (D (x,),),) {fun getA = => new C() {fun foo = x: T, => x}}} +//│ Parsed: {class B‹T›() {}; class C() {}; class D(y: Int,) {}; class A‹T, U›(x: Int,): & (& ('{' {a1: Int} '}',) (B‹T›,),) (D (x,),) {fun getA = => new C() {fun foo = x: T, => x}}} //│ Parsed: -//│ TypingUnit(NuTypeDef(class, B, (TypeName(T)), Tup(), (), TypingUnit()), NuTypeDef(class, C, (), Tup(), (), TypingUnit()), NuTypeDef(class, D, (), Tup(y: Var(Int)), (), TypingUnit()), NuTypeDef(class, A, (TypeName(T), TypeName(U)), Tup(x: Var(Int)), (App(App(Var(&), Tup(_: Bra(rcd = true, Rcd(Var(a1) = Var(Int))))), Tup(_: App(App(Var(&), Tup(_: TyApp(Var(B), List(TypeName(T))))), Tup(_: App(Var(D), Tup(_: Var(x)))))))), TypingUnit(NuFunDef(None, getA, [], Lam(Tup(), New(Some((TypeName(C),)), TypingUnit(List(fun foo = x: T, => x)))))))) +//│ TypingUnit(NuTypeDef(class, B, (TypeName(T)), Tup(), (), TypingUnit()), NuTypeDef(class, C, (), Tup(), (), TypingUnit()), NuTypeDef(class, D, (), Tup(y: Var(Int)), (), TypingUnit()), NuTypeDef(class, A, (TypeName(T), TypeName(U)), Tup(x: Var(Int)), (App(App(Var(&), Tup(_: App(App(Var(&), Tup(_: Bra(rcd = true, Rcd(Var(a1) = Var(Int))))), Tup(_: TyApp(Var(B), List(TypeName(T))))))), Tup(_: App(Var(D), Tup(_: Var(x)))))), TypingUnit(NuFunDef(None, getA, [], Lam(Tup(), New(Some((TypeName(C),)), TypingUnit(List(fun foo = x: T, => x)))))))) //│ Lifted: //│ TypingUnit { //│ class B$1[T]() {} //│ class C$2() {} //│ class D$3(y: Int,) {} //│ class A$4_C$1$5[T](par$A$4,): C$2 () {fun foo = x: T, => x} -//│ class A$4[T,U](x: Int,): & ('{' {a1: Int} '}',) (& (B$1 ()‹T›,) (D$3 ((this).x,),),) {fun getA = => {new A$4_C$1$5(this,) {}}} +//│ class A$4[T,U](x: Int,): & (& ('{' {a1: Int} '}',) (B$1 ()‹T›,),) (D$3 ((this).x,),) {fun getA = => {new A$4_C$1$5(this,) {}}} //│ } // │ TypingUnit(NuTypeDef(class, B, (TypeName(T)), Tup(), (), TypingUnit()), NuTypeDef(class, C, (), Tup(), (), TypingUnit()), NuTypeDef(class, A, (TypeName(T), TypeName(U)), Tup(x: Var(Int)), (App(App(Var(&), Tup(_: Bra(rcd = true, Rcd(Var(a1) = Var(Int)})))), Tup(_: TyApp(Var(B), List(TypeName(T)))))), TypingUnit(NuFunDef(None, getA, [], Lam(Tup(), New(Some((TypeName(C),)), TypingUnit(List(fun foo = x: T, => x)))))))) @@ -269,7 +269,7 @@ new B{ //│ |#class| |A| |{|→|#fun| |getA| |#=| |0|↵|#fun| |funcA| |#=| |10|←|↵|}|↵|#class| |B|#:| |A|{|→|#fun| |getA| |#=| |1|↵|#fun| |funcB| |#=| |11|←|↵|}|↵|#new| |A|↵|#new| |B|↵|#fun| |f|(|x|)| |#=| |#if| |x| |is| |A| |#then| |0| |#else| |1|↵|f|(|#new| |A|{|→|#fun| |getA| |#=| |2|←|↵|}|)|↵|#new| |B|{|→|#fun| |getA| |#=| |funcB|←|↵|}| //│ Parsed: {class A() {fun getA = 0; fun funcA = 10}; class B(): A {fun getA = 1; fun funcB = 11}; new A() {}; new B() {}; fun f = x, => if (is (x,) (A,)) then 0 else 1; f (new A() {fun getA = 2},); new B() {fun getA = funcB}} //│ Parsed: -//│ TypingUnit(NuTypeDef(class, A, (), Tup(), (), TypingUnit(NuFunDef(None, getA, [], IntLit(0)), NuFunDef(None, funcA, [], IntLit(10)))), NuTypeDef(class, B, (), Tup(), (Var(A)), TypingUnit(NuFunDef(None, getA, [], IntLit(1)), NuFunDef(None, funcB, [], IntLit(11)))), New(Some((TypeName(A),)), TypingUnit(List())), New(Some((TypeName(B),)), TypingUnit(List())), NuFunDef(None, f, [], Lam(Tup(_: Var(x)), If((is (x,) (A,)) then 0, Some(IntLit(1))))), App(Var(f), Tup(_: New(Some((TypeName(A),)), TypingUnit(List(fun getA = 2))))), New(Some((TypeName(B),)), TypingUnit(List(fun getA = funcB)))) +//│ TypingUnit(NuTypeDef(class, A, (), Tup(), (), TypingUnit(NuFunDef(None, getA, [], IntLit(0)), NuFunDef(None, funcA, [], IntLit(10)))), NuTypeDef(class, B, (), Tup(), (Var(A)), TypingUnit(NuFunDef(None, getA, [], IntLit(1)), NuFunDef(None, funcB, [], IntLit(11)))), New(Some((TypeName(A),)), TypingUnit(List())), New(Some((TypeName(B),)), TypingUnit(List())), NuFunDef(None, f, [], Lam(Tup(_: Var(x)), If(IfThen(App(App(Var(is), Tup(_: Var(x))), Tup(_: Var(A))), IntLit(0), Some(IntLit(1))))), App(Var(f), Tup(_: New(Some((TypeName(A),)), TypingUnit(List(fun getA = 2))))), New(Some((TypeName(B),)), TypingUnit(List(fun getA = funcB)))) //│ Lifted: //│ TypingUnit { //│ class A$1() {fun getA = 0; fun funcA = 10} diff --git a/compiler/shared/test/diff/LifterBlks.mls b/compiler/shared/test/diff/LifterBlks.mls index db4520d79d..a8bde99675 100644 --- a/compiler/shared/test/diff/LifterBlks.mls +++ b/compiler/shared/test/diff/LifterBlks.mls @@ -132,7 +132,7 @@ fun f(x,y,z) = fun boo = (new A).bar1 + B().bar2 + z } //│ |#fun| |f|(|x|,|y|,|z|)| |#=| |→|#class| |C|{|→|#class| |A|{|→|#fun| |foo| |#=| |#new| |B|↵|#fun| |bar1| |#=| |x|←|↵|}|↵|#class| |B|{|→|#fun| |foo| |#=| |#new| |A|↵|#fun| |bar2| |#=| |y|←|↵|}|↵|#fun| |boo| |#=| |(|#new| |A|)|.bar1| |+| |B|(||)|.bar2| |+| |z|←|↵|}|←| -//│ Parsed: {fun f = x, y, z, => {class C() {class A() {fun foo = new B() {}; fun bar1 = x}; class B() {fun foo = new A() {}; fun bar2 = y}; fun boo = + (('(' new A() {}, ')').bar1,) (+ ((B ()).bar2,) (z,),)}}} +//│ Parsed: {fun f = x, y, z, => {class C() {class A() {fun foo = new B() {}; fun bar1 = x}; class B() {fun foo = new A() {}; fun bar2 = y}; fun boo = + (+ (('(' new A() {}, ')').bar1,) ((B ()).bar2,),) (z,)}}} //│ Parsed: //│ TypingUnit(NuFunDef(None, f, [], Lam(Tup(_: Var(x), _: Var(y), _: Var(z)), Blk(...)))) //│ Lifted: @@ -146,7 +146,7 @@ fun f(x,y,z) = //│ fun bar2 = ((this).par$C$1).y //│ } //│ class C$1(x, y, z,) { -//│ fun boo = + (('(' new C$1_A$2(this,) {}, ')').bar1,) (+ ((C$1_B$3 (this,)).bar2,) ((this).z,),) +//│ fun boo = + (+ (('(' new C$1_A$2(this,) {}, ')').bar1,) ((C$1_B$3 (this,)).bar2,),) ((this).z,) //│ } //│ fun f = x, y, z, => {} //│ } diff --git a/js/src/main/scala/Main.scala b/js/src/main/scala/Main.scala index b220ff0e64..826404339a 100644 --- a/js/src/main/scala/Main.scala +++ b/js/src/main/scala/Main.scala @@ -311,7 +311,7 @@ object Main { subsume(ty_sch, sign)(ctx, raise, TypeProvenance(d.toLoc, "def definition")) // Note: keeping the less precise declared type signature here (no ctx update) case N => - ctx += nme.name -> ty_sch + ctx += nme.name -> VarSymbol(ty_sch, nme) } res ++= formatBinding(d.nme.name, ty_sch) results append S(d.nme.name) -> htmlize(getType(ty_sch).show) @@ -327,14 +327,14 @@ object Main { } val ty_sch = PolymorphicType(0, typeType(rhs)(ctx.nextLevel, raise, vars = tps.map(tp => tp.name -> freshVar(noProv/*FIXME*/)(1)).toMap)) - ctx += nme.name -> ty_sch + ctx += nme.name -> VarSymbol(ty_sch, nme) declared += nme -> ty_sch results append S(d.nme.name) -> htmlize(getType(ty_sch).show) case s: DesugaredStatement => typer.typeStatement(s, allowPure = true) match { case R(binds) => binds.foreach { case (nme, pty) => - ctx += nme -> pty + ctx += nme -> VarSymbol(pty, Var(nme)) res ++= formatBinding(nme, pty) results append S(nme) -> htmlize(getType(pty).show) } @@ -342,7 +342,7 @@ object Main { val exp = getType(pty) if (exp =/= TypeName("unit")) { val nme = "res" - ctx += nme -> pty + ctx += nme -> VarSymbol(pty, Var(nme)) res ++= formatBinding(nme, pty) results append N -> htmlize(getType(pty).show) } diff --git a/shared/src/main/scala/mlscript/JSBackend.scala b/shared/src/main/scala/mlscript/JSBackend.scala index 4f42d57d96..c6c39ed8d2 100644 --- a/shared/src/main/scala/mlscript/JSBackend.scala +++ b/shared/src/main/scala/mlscript/JSBackend.scala @@ -137,6 +137,7 @@ class JSBackend(allowUnresolvedSymbols: Boolean) { * Translate MLscript terms into JavaScript expressions. */ protected def translateTerm(term: Term)(implicit scope: Scope): JSExpr = term match { + case _ if term.desugaredTerm.isDefined => translateTerm(term.desugaredTerm.get) case Var(name) => translateVar(name, false) case Lam(params, body) => val lamScope = scope.derive("Lam") @@ -178,15 +179,17 @@ class JSBackend(allowUnresolvedSymbols: Boolean) { ) case Blk(stmts) => val blkScope = scope.derive("Blk") + val flattened = stmts.iterator.flatMap(_.desugared._2).toList JSImmEvalFn( N, Nil, - R(blkScope.tempVars `with` (stmts flatMap (_.desugared._2) map { - case t: Term => JSExprStmt(translateTerm(t)) + R(blkScope.tempVars `with` (flattened.iterator.zipWithIndex.map { + case (t: Term, index) if index + 1 == flattened.length => translateTerm(t)(blkScope).`return` + case (t: Term, index) => JSExprStmt(translateTerm(t)(blkScope)) // TODO: find out if we need to support this. - case _: Def | _: TypeDef | _: NuFunDef /* | _: NuTypeDef */ => - throw CodeGenError("unexpected definitions in blocks") - })), + case (_: Def | _: TypeDef | _: NuFunDef /* | _: NuTypeDef */, _) => + throw CodeGenError("unsupported definitions in blocks") + }.toList)), Nil ) // Pattern match with only one branch -> comma expression @@ -235,7 +238,15 @@ class JSBackend(allowUnresolvedSymbols: Boolean) { case _ => throw CodeGenError(s"illegal assignemnt left-hand side: ${inspect(lhs)}") } - case _: Bind | _: Test | If(_, _) | New(_, _) | TyApp(_, _) | _: Splc => + case iff: If => + throw CodeGenError(s"if expression has not been desugared") + case New(N, TypingUnit(Nil)) => JSRecord(Nil) + case New(S(TypeName(className) -> Tup(args)), TypingUnit(Nil)) => + val callee = translateVar(className, true) + callee(args.map { case (_, Fld(_, _, arg)) => translateTerm(arg) }: _*) + case New(_, TypingUnit(_)) => + throw CodeGenError("custom class body is not supported yet") + case _: Bind | _: Test | If(_, _) | TyApp(_, _) | _: Splc => throw CodeGenError(s"cannot generate code for term ${inspect(term)}") } @@ -629,14 +640,15 @@ class JSTestBackend extends JSBackend(allowUnresolvedSymbols = false) { // Generate statements. val queries = otherStmts.map { case Def(recursive, Var(name), L(body), isByname) => + val bodyIsLam = body match { case _: Lam => true case _ => false } (if (recursive) { val isByvalueRecIn = if (isByname) None else Some(true) - val sym = scope.declareValue(name, isByvalueRecIn, body.isInstanceOf[Lam]) + val sym = scope.declareValue(name, isByvalueRecIn, bodyIsLam) try { val translated = translateTerm(body) scope.unregisterSymbol(sym) val isByvalueRecOut = if (isByname) None else Some(false) - R((translated, scope.declareValue(name, isByvalueRecOut, body.isInstanceOf[Lam]))) + R((translated, scope.declareValue(name, isByvalueRecOut, bodyIsLam))) } catch { case e: UnimplementedError => scope.stubize(sym, e.symbol) @@ -644,7 +656,7 @@ class JSTestBackend extends JSBackend(allowUnresolvedSymbols = false) { case e: Throwable => scope.unregisterSymbol(sym) val isByvalueRecOut = if (isByname) None else Some(false) - scope.declareValue(name, isByvalueRecOut, body.isInstanceOf[Lam]) + scope.declareValue(name, isByvalueRecOut, bodyIsLam) throw e } } else { @@ -655,7 +667,7 @@ class JSTestBackend extends JSBackend(allowUnresolvedSymbols = false) { case e: Throwable => throw e }) map { val isByvalueRec = if (isByname) None else Some(false) - expr => (expr, scope.declareValue(name, isByvalueRec, body.isInstanceOf[Lam])) + expr => (expr, scope.declareValue(name, isByvalueRec, bodyIsLam)) } }) match { case R((originalExpr, sym)) => @@ -736,7 +748,9 @@ object JSTestBackend { /** * Represents the result of code generation. */ - abstract class Result + abstract class Result { + def showFirstResult(prefixLength: Int): Unit = () + } /** * Emitted code. diff --git a/shared/src/main/scala/mlscript/NewParser.scala b/shared/src/main/scala/mlscript/NewParser.scala index 793e59fbbf..1354c92cfb 100644 --- a/shared/src/main/scala/mlscript/NewParser.scala +++ b/shared/src/main/scala/mlscript/NewParser.scala @@ -93,6 +93,7 @@ abstract class NewParser(origin: Origin, tokens: Ls[Stroken -> Loc], raiseFun: D "", "", "", + "", // ^ for keywords ",", ";", @@ -116,10 +117,11 @@ abstract class NewParser(origin: Origin, tokens: Ls[Stroken -> Loc], raiseFun: D def opCharPrec(opChar: Char): Int = prec(opChar) def opPrec(opStr: Str): (Int, Int) = opStr match { + case "is" => (4, 4) case "and" => (3, 3) case "or" => (2, 2) case _ if opStr.exists(_.isLetter) => - (4, 4) + (5, 5) case _ => val r = opStr.last (prec(opStr.head), prec(r) - (if (r === '@' || r === '/' || r === ',') 1 else 0)) @@ -652,25 +654,26 @@ abstract class NewParser(origin: Origin, tokens: Ls[Stroken -> Loc], raiseFun: D } case _ => ??? }) - exprCont(res, 0, allowNewlines) + exprCont(res, prec, allowNewlines) case (br @ BRACKETS(Square, toks), loc) :: _ => consume val idx = rec(toks, S(br.innerLoc), "subscript").concludeWith(_.expr(0)) val res = Subs(acc, idx.withLoc(S(loc))) - exprCont(res, 0, allowNewlines) + exprCont(res, prec, allowNewlines) case (br @ BRACKETS(Round, toks), loc) :: _ => consume val as = rec(toks, S(br.innerLoc), br.describe).concludeWith(_.argsMaybeIndented()) val res = App(acc, Tup(as).withLoc(S(loc))) - exprCont(res, 0, allowNewlines) + exprCont(res, prec, allowNewlines) + case (KEYWORD("of"), _) :: _ => consume val as = argsMaybeIndented() // val as = argsOrIf(Nil) // TODO val res = App(acc, Tup(as)) - exprCont(res, 0, allowNewlines) + exprCont(res, prec, allowNewlines) case c @ (h :: _) if (h._1 match { case KEYWORD(";" | "of") | BRACKETS(Round | Square, _) @@ -686,7 +689,7 @@ abstract class NewParser(origin: Origin, tokens: Ls[Stroken -> Loc], raiseFun: D val res = App(acc, Tup(as)) raise(WarningReport(msg"Paren-less applications should use the 'of' keyword" -> res.toLoc :: Nil)) - exprCont(res, 0, allowNewlines) + exprCont(res, prec, allowNewlines) case _ => R(acc) } diff --git a/shared/src/main/scala/mlscript/TypeDefs.scala b/shared/src/main/scala/mlscript/TypeDefs.scala index 3c2564c436..71acc08266 100644 --- a/shared/src/main/scala/mlscript/TypeDefs.scala +++ b/shared/src/main/scala/mlscript/TypeDefs.scala @@ -181,7 +181,7 @@ class TypeDefs extends NuTypeDefs { self: Typer => val (bodyTy, tvars) = typeType2(td.body, simplify = false)(ctx.copy(lvl = 0), raise, tparamsargs.map(_.name -> _).toMap, newDefsInfo) val td1 = TypeDef(td.kind, td.nme, tparamsargs.toList, tvars, bodyTy, - td.mthDecls, td.mthDefs, baseClassesOf(td), td.toLoc, Nil) + td.mthDecls, td.mthDefs, baseClassesOf(td), td.toLoc, td.positionals.map(_.name)) allDefs += n -> td1 S(td1) } @@ -337,7 +337,7 @@ class TypeDefs extends NuTypeDefs { self: Typer => singleTup(tv), tv & nomTag & RecordType.mk(tparamTags)(noProv) )(originProv(td.nme.toLoc, "trait constructor", td.nme.name))) } - ctx += n.name -> ctor + ctx += n.name -> VarSymbol(ctor, Var(n.name)) } true } @@ -491,7 +491,7 @@ class TypeDefs extends NuTypeDefs { self: Typer => def go(md: MethodDef[_ <: Term \/ Type]): (Str, MethodType) = { val thisTag = TraitTag(Var("this"))(noProv) val thisTy = thisTag & tr - thisCtx += "this" -> thisTy + thisCtx += "this" -> VarSymbol(thisTy, Var("this")) val MethodDef(rec, prt, nme, tparams, rhs) = md val prov: TypeProvenance = tp(md.toLoc, (if (!top) "inherited " else "") diff --git a/shared/src/main/scala/mlscript/Typer.scala b/shared/src/main/scala/mlscript/Typer.scala index 8712af4827..75875f1be7 100644 --- a/shared/src/main/scala/mlscript/Typer.scala +++ b/shared/src/main/scala/mlscript/Typer.scala @@ -14,7 +14,7 @@ import mlscript.Message._ * In order to turn the resulting CompactType into a mlscript.Type, we use `expandCompactType`. */ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) - extends TypeDefs with TypeSimplifier { + extends ucs.Desugarer with TypeSimplifier { def funkyTuples: Bool = false def doFactorize: Bool = false @@ -65,7 +65,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) object Ctx { def init: Ctx = Ctx( parent = N, - env = MutMap.from(builtinBindings), + env = MutMap.from(builtinBindings.iterator.map(nt => nt._1 -> VarSymbol(nt._2, Var(nt._1)))), mthEnv = MutMap.empty, lvl = 0, inPattern = false, @@ -189,6 +189,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) "+" -> intBinOpTy, "-" -> intBinOpTy, "*" -> intBinOpTy, + "%" -> intBinOpTy, "/" -> numberBinOpTy, "<" -> numberBinPred, ">" -> numberBinPred, @@ -288,7 +289,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) case TypeName("this") => ctx.env.getOrElse("this", err(msg"undeclared this" -> ty.toLoc :: Nil)) match { case AbstractConstructor(_, _) => die - case t: TypeScheme => t.instantiate + case VarSymbol(t: TypeScheme, _) => t.instantiate } case tn @ TypeTag(name) => rec(TypeName(name.decapitalize)) case tn @ TypeName(name) => @@ -355,14 +356,15 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) def typeStatement(s: DesugaredStatement, allowPure: Bool) - (implicit ctx: Ctx, raise: Raise): PolymorphicType \/ Ls[Binding] = s match { + (implicit ctx: Ctx, raise: Raise): PolymorphicType \/ Opt[Binding] = s match { case Def(false, Var("_"), L(rhs), isByname) => typeStatement(rhs, allowPure) case Def(isrec, nme, L(rhs), isByname) => // TODO reject R(..) if (nme.name === "_") err(msg"Illegal definition name: ${nme.name}", nme.toLoc)(raise) val ty_sch = typeLetRhs(isrec, nme.name, rhs) - ctx += nme.name -> ty_sch - R(nme.name -> ty_sch :: Nil) + nme.uid = S(nextUid) + ctx += nme.name -> VarSymbol(ty_sch, nme) + R(S(nme.name -> ty_sch)) case t @ Tup(fs) if !allowPure => // Note: not sure this is still used! val thing = fs match { case (S(_), _) :: Nil => "field" @@ -387,7 +389,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) L(PolymorphicType(0, ty)) case _ => err(msg"Illegal position for this ${s.describe} statement.", s.toLoc)(raise) - R(Nil) + R(N) } /** Infer the type of a let binding right-hand side. */ @@ -402,7 +404,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) // TypeProvenance(rhs.toLoc, "let-bound value"), S(nme) )(lvl + 1) - ctx += nme -> e_ty + ctx += nme -> VarSymbol(e_ty, Var(nme)) val ty = typeTerm(rhs)(ctx.nextLevel, raise, vars) constrain(ty, e_ty)(raise, TypeProvenance(rhs.toLoc, "binding of " + rhs.describe), ctx) e_ty @@ -418,12 +420,12 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) } // TODO also prevent rebinding of "not" - val reservedNames: Set[Str] = Set("|", "&", "~", ",", "neg", "and", "or") + val reservedNames: Set[Str] = Set("|", "&", "~", ",", "neg", "and", "or", "is") object ValidVar { def unapply(v: Var)(implicit raise: Raise): S[Str] = S { if (reservedNames(v.name)) - err(s"Illegal use of ${if (v.name.head.isLetter) "keyword" else "operator"}: " + v.name, + err(s"Illegal use of reserved operator: " + v.name, v.toLoc)(raise) v.name } @@ -431,11 +433,12 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) object ValidPatVar { def unapply(v: Var)(implicit ctx: Ctx, raise: Raise): Opt[Str] = if (ctx.inPattern && v.isPatVar) { - ctx.parent.dlof(_.get(v.name))(N) |>? { case S(ts: TypeScheme) => ts.instantiate(0).unwrapProxies } |>? { - case S(ClassTag(Var(v.name), _)) => - warn(msg"Variable name '${v.name}' already names a symbol in scope. " + - s"If you want to refer to that symbol, you can use `scope.${v.name}`; " + - s"if not, give your future readers a break and use another name :^)", v.toLoc) + ctx.parent.dlof(_.get(v.name))(N) |>? { case S(VarSymbol(ts: TypeScheme, _)) => + ts.instantiate(0).unwrapProxies } |>? { + case S(ClassTag(Var(v.name), _)) => + warn(msg"Variable name '${v.name}' already names a symbol in scope. " + + s"If you want to refer to that symbol, you can use `scope.${v.name}`; " + + s"if not, give your future readers a break and use another name :^)", v.toLoc) } ValidVar.unapply(v) } else N @@ -485,8 +488,13 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) case (v @ ValidPatVar(nme)) => val prov = tp(if (verboseConstraintProvenanceHints) v.toLoc else N, "variable") // Note: only look at ctx.env, and not the outer ones! - ctx.env.get(nme).collect { case ts: TypeScheme => ts.instantiate } - .getOrElse(new TypeVariable(lvl, Nil, Nil)(prov).tap(ctx += nme -> _)) + ctx.env.get(nme).collect { case VarSymbol(ts, dv) => assert(v.uid.isDefined); v.uid = dv.uid; ts.instantiate } + .getOrElse { + val res = new TypeVariable(lvl, Nil, Nil)(prov) + v.uid = S(nextUid) + ctx += nme -> VarSymbol(res, v) + res + } case v @ ValidVar(name) => val ty = ctx.get(name).fold(err("identifier not found: " + name, term.toLoc): TypeScheme) { case AbstractConstructor(absMths, traitWithMths) => @@ -501,7 +509,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) :: absMths.map { case mn => msg"Hint: method ${mn.name} is abstract" -> mn.toLoc }.toList ) ) - case ty: TypeScheme => ty + case VarSymbol(ty: TypeScheme, _) => ty }.instantiate mkProxy(ty, prov) // ^ TODO maybe use a description passed in param? @@ -608,17 +616,18 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) err(msg"Unsupported pattern shape${ if (dbg) " ("+pat.getClass.toString+")" else ""}:", pat.toLoc)(raise) case Lam(pat, body) => - val newBindings = mutable.Map.empty[Str, TypeVariable] val newCtx = ctx.nest val param_ty = typePattern(pat)(newCtx, raise, vars) - newCtx ++= newBindings val body_ty = typeTerm(body)(newCtx, raise, vars) FunctionType(param_ty, body_ty)(tp(term.toLoc, "function")) - case App(App(Var("and"), lhs), rhs) => - val lhs_ty = typeTerm(lhs) - val newCtx = ctx.nest // TODO use - val rhs_ty = typeTerm(lhs) - ??? // TODO + case App(App(Var("is"), _), _) => + val desug = If(IfThen(term, Var("true")), S(Var("false"))) + term.desugaredTerm = S(desug) + typeTerm(desug) + case App(App(Var("and"), Tup(_ -> Fld(_, _, lhs) :: Nil)), Tup(_ -> Fld(_, _, rhs) :: Nil)) => + val desug = If(IfThen(lhs, rhs), S(Var("false"))) + term.desugaredTerm = S(desug) + typeTerm(desug) case App(f, a) => val f_ty = typeTerm(f) val a_ty = typeTerm(a) @@ -683,7 +692,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) case Let(isrec, nme, rhs, bod) => val n_ty = typeLetRhs(isrec, nme.name, rhs) val newCtx = ctx.nest - newCtx += nme.name -> n_ty + newCtx += nme.name -> VarSymbol(n_ty, nme) typeTerm(bod)(newCtx, raise) // case Blk(s :: stmts) => // val (newCtx, ty) = typeStatement(s) @@ -715,8 +724,20 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) case ((a_ty, tv), req) => a_ty & tv | req & a_ty.neg() } con(s_ty, req, cs_ty) - case If(_, _) => - ??? // TODO + case iff @ If(body, fallback) => + import mlscript.ucs._ + try { + val caseTree = MutCaseOf.build(desugarIf(body, fallback)) + println("The mutable CaseOf tree") + MutCaseOf.show(caseTree).foreach(println(_)) + checkExhaustive(caseTree, N)(summarizePatterns(caseTree), ctx, raise) + val desugared = MutCaseOf.toTerm(caseTree) + println(s"Desugared term: ${desugared.print(false)}") + iff.desugaredTerm = S(desugared) + typeTerm(desugared) + } catch { + case e: DesugaringException => err(e.messages) + } case New(S((nmedTy, trm)), TypingUnit(Nil)) => typeTerm(App(Var(nmedTy.base.name).withLocOf(nmedTy), trm)) case New(base, args) => ??? @@ -733,7 +754,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) val newCtx = ctx.nest scrutVar match { case Some(v) => - newCtx += v.name -> fv + newCtx += v.name -> VarSymbol(fv, v) val b_ty = typeTerm(b)(newCtx, raise) (fv -> TopType :: Nil) -> b_ty case _ => @@ -765,7 +786,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) val tv = freshVar(tp(v.toLoc, "refined scrutinee"), // S(v.name), // this one seems a bit excessive ) - newCtx += v.name -> tv + newCtx += v.name -> VarSymbol(tv, v) val bod_ty = typeTerm(bod)(newCtx, raise) (patTy -> tv, bod_ty, typeArms(scrutVar, rest)) case N => @@ -807,7 +828,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) // constrain(ty, t_ty)(raise, prov) constrain(t_ty, ty)(raise, prov, ctx) - ctx += nme.name -> t_ty + ctx += nme.name -> VarSymbol(t_ty, nme) t_ty // ty @@ -815,7 +836,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) // ComposedType(true, t_ty, ty)(prov) // loops! case S(nme) => - ctx += nme.name -> ty + ctx += nme.name -> VarSymbol(ty, nme) ty case _ => ty @@ -833,7 +854,7 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) val (diags, desug) = s.desugared diags.foreach(raise) val newBindings = desug.flatMap(typeStatement(_, allowPure = false).toOption) - ctx ++= newBindings.flatten + ctx ++= newBindings.iterator.flatten.map(nt => nt._1 -> VarSymbol(nt._2, Var(nt._1))) typeTerms(sts, rcd, fields) case Nil => if (rcd) { @@ -925,4 +946,11 @@ class Typer(var dbg: Boolean, var verbose: Bool, var explainErrors: Bool) else Constrained(res, bounds) } + + private var curUid: Int = 0 + def nextUid: Int = { + val res = curUid + curUid += 1 + res + } } diff --git a/shared/src/main/scala/mlscript/TyperDatatypes.scala b/shared/src/main/scala/mlscript/TyperDatatypes.scala index 099e4f76f0..9340c334dc 100644 --- a/shared/src/main/scala/mlscript/TyperDatatypes.scala +++ b/shared/src/main/scala/mlscript/TyperDatatypes.scala @@ -21,16 +21,18 @@ abstract class TyperDatatypes extends TyperHelpers { self: Typer => override def toString: Str = (if (isOrigin) "o: " else "") + "‹"+loco.fold(desc)(desc+":"+_)+"›" } type TP = TypeProvenance - + sealed abstract class TypeInfo /** A type for abstract classes that is used to check and throw * errors if the abstract class is being instantiated */ case class AbstractConstructor(absMths: Set[Var], isTraitWithMethods: Bool) extends TypeInfo + case class VarSymbol(ty: TypeScheme, definingVar: Var) extends TypeInfo + /** A type that potentially contains universally quantified type variables, * and which can be isntantiated to a given level. */ - sealed abstract class TypeScheme extends TypeInfo { + sealed abstract class TypeScheme { def uninstantiatedBody: SimpleType def instantiate(implicit lvl: Int): SimpleType } diff --git a/shared/src/main/scala/mlscript/TyperHelpers.scala b/shared/src/main/scala/mlscript/TyperHelpers.scala index f1afb3e21f..bb04298074 100644 --- a/shared/src/main/scala/mlscript/TyperHelpers.scala +++ b/shared/src/main/scala/mlscript/TyperHelpers.scala @@ -21,7 +21,7 @@ abstract class TyperHelpers { Typer: Typer => constructedTypes = 0 } - private val noPostTrace: Any => String = _ => "" + protected val noPostTrace: Any => String = _ => "" protected var indent = 0 def trace[T](pre: => String)(thunk: => T)(post: T => String = noPostTrace): T = { diff --git a/shared/src/main/scala/mlscript/codegen/Helpers.scala b/shared/src/main/scala/mlscript/codegen/Helpers.scala index 8399e19ed4..07c55d212f 100644 --- a/shared/src/main/scala/mlscript/codegen/Helpers.scala +++ b/shared/src/main/scala/mlscript/codegen/Helpers.scala @@ -48,13 +48,33 @@ object Helpers { case Splc(fs) => val elems = fs.map{case L(l) => s"...${inspect(l)}" case R(Fld(_, _, r)) => inspect(r)}.mkString(", ") s"Splc($elems)" - case If(bod, els) => s"If($bod, ${els.map(inspect)})" + case If(bod, els) => s"If(${inspect(bod)}, ${els.map(inspect)})" case New(base, body) => s"New(${base}, ${body})" case TyApp(base, targs) => s"TyApp(${inspect(base)}, ${targs})" case Def(rec, nme, rhs, isByname) => s"Def($rec, $nme, ${rhs.fold(inspect, "" + _)}, $isByname)" } + def inspect(body: IfBody): Str = body match { + case IfElse(expr) => s"IfElse(${inspect(expr)}" + case IfThen(expr, rhs) => s"IfThen(${inspect(expr)}, ${inspect(rhs)}" + case IfBlock(lines) => s"IfBlock(${ + lines.iterator.map { + case L(body) => inspect(body) + case R(NuFunDef(S(isRec), nme, _, L(rhs))) => + s"Let($isRec, ${nme.name}, ${inspect(rhs)})" + case R(_) => ??? + }.mkString(";") + })" + case IfOpsApp(lhs, opsRhss) => s"IfOpsApp(${inspect(lhs)}, ${ + opsRhss.iterator.map { case (op, body) => + s"$op -> ${inspect(body)}" + } + }".mkString("; ") + case IfLet(isRec, name, rhs, body) => ??? + case IfOpApp(lhs, op, rhs) => + s"IfOpApp(${inspect(lhs)}, ${inspect(op)}, ${inspect(rhs)}" + } def inspect(t: TypingUnit): Str = t.entities.iterator .map { case term: Term => inspect(term) diff --git a/shared/src/main/scala/mlscript/helpers.scala b/shared/src/main/scala/mlscript/helpers.scala index 618083a442..e457a0b937 100644 --- a/shared/src/main/scala/mlscript/helpers.scala +++ b/shared/src/main/scala/mlscript/helpers.scala @@ -331,40 +331,53 @@ trait TypeNameImpl extends Ordered[TypeName] { self: TypeName => trait TermImpl extends StatementImpl { self: Term => val original: this.type = this + + /** Used by code generation when the typer desugars this term into a different term. */ + var desugaredTerm: Opt[Term] = N + + private var sugaredTerm: Opt[Term] = N - def describe: Str = this match { - case Bra(true, Tup(_ :: _ :: _) | Tup((S(_), _) :: _) | Blk(_)) => "record" - case Bra(_, trm) => trm.describe - case Blk((trm: Term) :: Nil) => trm.describe - case Blk(_) => "block of statements" - case IntLit(value) => "integer literal" - case DecLit(value) => "decimal literal" - case StrLit(value) => "string literal" - case UnitLit(value) => if (value) "undefined literal" else "null literal" - case Var(name) => "reference" // "variable reference" - case Asc(trm, ty) => "type ascription" - case Lam(name, rhs) => "lambda expression" - case App(OpApp(Var("|"), lhs), rhs) => "type union" - case App(OpApp(Var("&"), lhs), rhs) => "type intersection" - case App(OpApp(op, lhs), rhs) => "operator application" - case OpApp(op, lhs) => "operator application" - case App(lhs, rhs) => "application" - case Rcd(fields) => "record" - case Sel(receiver, fieldName) => "field selection" - case Let(isRec, name, rhs, body) => "let binding" - case Tup((N, Fld(_, _, x)) :: Nil) => x.describe - case Tup((S(_), x) :: Nil) => "binding" - case Tup(xs) => "tuple" - case Bind(l, r) => "'as' binding" - case Test(l, r) => "'is' test" - case With(t, fs) => "`with` extension" - case CaseOf(scrut, cases) => "`case` expression" - case Subs(arr, idx) => "array access" - case Assign(lhs, rhs) => "assignment" - case Splc(fs) => "splice" - case New(h, b) => "object instantiation" - case If(_, _) => "if-else block" - case TyApp(_, _) => "type application" + def desugaredFrom(term: Term): this.type = { + sugaredTerm = S(term) + withLocOf(term) + } + + def describe: Str = sugaredTerm match { + case S(t) => t.describe + case N => this match { + case Bra(true, Tup(_ :: _ :: _) | Tup((S(_), _) :: _) | Blk(_)) => "record" + case Bra(_, trm) => trm.describe + case Blk((trm: Term) :: Nil) => trm.describe + case Blk(_) => "block of statements" + case IntLit(value) => "integer literal" + case DecLit(value) => "decimal literal" + case StrLit(value) => "string literal" + case UnitLit(value) => if (value) "undefined literal" else "null literal" + case Var(name) => "reference" // "variable reference" + case Asc(trm, ty) => "type ascription" + case Lam(name, rhs) => "lambda expression" + case App(OpApp(Var("|"), lhs), rhs) => "type union" + case App(OpApp(Var("&"), lhs), rhs) => "type intersection" + case App(OpApp(op, lhs), rhs) => "operator application" + case OpApp(op, lhs) => "operator application" + case App(lhs, rhs) => "application" + case Rcd(fields) => "record" + case Sel(receiver, fieldName) => "field selection" + case Let(isRec, name, rhs, body) => "let binding" + case Tup((N, Fld(_, _, x)) :: Nil) => x.describe + case Tup((S(_), x) :: Nil) => "binding" + case Tup(xs) => "tuple" + case Bind(l, r) => "'as' binding" + case Test(l, r) => "'is' test" + case With(t, fs) => "`with` extension" + case CaseOf(scrut, cases) => "`case` expression" + case Subs(arr, idx) => "array access" + case Assign(lhs, rhs) => "assignment" + case Splc(fs) => "splice" + case New(h, b) => "object instantiation" + case If(_, _) => "if-else block" + case TyApp(_, _) => "type application" + } } override def toString: Str = print(false) @@ -401,7 +414,8 @@ trait TermImpl extends StatementImpl { self: Term => case Bind(l, r) => s"$l as $r" |> bra case Test(l, r) => s"$l is $r" |> bra case With(t, fs) => s"$t with $fs" |> bra - case CaseOf(s, c) => s"case $s of $c" |> bra + case CaseOf(s, c) => + s"case $s of { ${c.print(true)} }" |> bra case Subs(a, i) => s"($a)[$i]" case Assign(lhs, rhs) => s" $lhs <- $rhs" |> bra case New(S((at, ar)), bod) => s"new ${at.show}($ar) ${bod.show}" |> bra @@ -474,6 +488,7 @@ trait LitImpl { self: Lit => trait VarImpl { self: Var => def isPatVar: Bool = name.head.isLetter && name.head.isLower && name =/= "true" && name =/= "false" + var uid: Opt[Int] = N } trait SimpleTermImpl extends Ordered[SimpleTerm] { self: SimpleTerm => @@ -745,7 +760,24 @@ trait CaseBranchesImpl extends Located { self: CaseBranches => case c: Case => c :: c.rest.toList case _ => Nil } - + + def print(isFirst: Bool): Str = this match { + case Case(pat, body, rest) => + (if (isFirst) { "" } else { "; " }) + + pat.print(false) + " => " + body.print(false) + rest.print(false) + case Wildcard(body) => + (if (isFirst) { "" } else { "; " }) + + "_ => " + body.print(false) + case NoCases => "" + } +} + +abstract class MatchCase + +object MatchCase { + final case class ClassPattern(name: Var, fields: Buffer[Var -> Var]) extends MatchCase + final case class TuplePattern(arity: Int, fields: Buffer[Int -> Var]) extends MatchCase + final case class BooleanTest(test: Term) extends MatchCase } trait IfBodyImpl extends Located { self: IfBody => @@ -780,3 +812,4 @@ trait IfBodyImpl extends Located { self: IfBody => } } + diff --git a/shared/src/main/scala/mlscript/ucs/Clause.scala b/shared/src/main/scala/mlscript/ucs/Clause.scala new file mode 100644 index 0000000000..d4c26b6b1d --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/Clause.scala @@ -0,0 +1,72 @@ +package mlscript.ucs + +import mlscript._ +import mlscript.utils._ +import mlscript.utils.shorthands._ +import scala.collection.mutable.Buffer + +/** + * A `Clause` represents a minimal unit of logical predicate in the UCS. + * There are three kinds of clauses: boolean test, class match, and tuple match. + */ +abstract class Clause { + /** + * Local interleaved let bindings declared before this condition. + */ + var bindings: Ls[(Bool, Var, Term)] = Nil + + /** + * Locations of terms that build this `Clause`. + * + * @return + */ + val locations: Ls[Loc] +} + +object Clause { + final case class MatchClass( + scrutinee: Scrutinee, + className: Var, + fields: Ls[Str -> Var] + )(override val locations: Ls[Loc]) extends Clause + + final case class MatchTuple( + scrutinee: Scrutinee, + arity: Int, + fields: Ls[Str -> Var] + )(override val locations: Ls[Loc]) extends Clause + + final case class BooleanTest(test: Term)(override val locations: Ls[Loc]) extends Clause + + def showBindings(bindings: Ls[(Bool, Var, Term)]): Str = + bindings match { + case Nil => "" + case bindings => bindings.map { + case (_, Var(name), _) => name + }.mkString("(", ", ", ")") + } + + + def showClauses(clauses: Iterable[Clause]): Str = { + clauses.iterator.map { clause => + (clause match { + case Clause.BooleanTest(test) => s"«$test»" + case Clause.MatchClass(scrutinee, Var(className), fields) => + s"«$scrutinee is $className»" + case Clause.MatchTuple(scrutinee, arity, fields) => + s"«$scrutinee is Tuple#$arity»" + }) + (if (clause.bindings.isEmpty) "" else " with " + showBindings(clause.bindings)) + }.mkString("", " and ", "") + } + + def print(println: (=> Any) => Unit, conjunctions: Iterable[Conjunction -> Term]): Unit = { + println("Flattened conjunctions") + conjunctions.foreach { case Conjunction(clauses, trailingBindings) -> term => + println("+ " + showClauses(clauses) + { + (if (trailingBindings.isEmpty) "" else " ") + + showBindings(trailingBindings) + + s" => $term" + }) + } + } +} diff --git a/shared/src/main/scala/mlscript/ucs/Conjunction.scala b/shared/src/main/scala/mlscript/ucs/Conjunction.scala new file mode 100644 index 0000000000..ff9c6a2fb3 --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/Conjunction.scala @@ -0,0 +1,94 @@ +package mlscript.ucs + +import mlscript._, utils._, shorthands._ +import Clause._, helpers._ +import scala.collection.mutable.Buffer + +/** + * A `Conjunction` represents a list of `Clause`s. + */ +final case class Conjunction(clauses: Ls[Clause], trailingBindings: Ls[(Bool, Var, Term)]) { + /** + * Concatenate two `Conjunction` together. + * + * The trailing bindings of the first `Conjunction` will be added to the + * first `Clause` of the second `Conjunction` + * + * @param lhs the left hand side value + * @param rhs the right hand side value + * @return the sititched `Conjunction` + */ + def +(rhs: Conjunction): Conjunction = { + val Conjunction(lhsClauses, lhsTailBindings) = this + val Conjunction(rhsClauses, rhsTailBindings) = rhs + rhsClauses match { + case Nil => Conjunction(lhsClauses, lhsTailBindings ::: rhsTailBindings) + case head :: _ => + head.bindings = lhsTailBindings ::: head.bindings + Conjunction(lhsClauses ::: rhsClauses, rhsTailBindings) + } + } + + /** + * This is a shorthand if you only have clauses. + * + * @param suffix the list of clauses to append to this conjunction + * @return a new conjunction with clauses from `this` and `suffix` + */ + def +(suffix: Ls[Clause]): Conjunction = { + suffix match { + case Nil => this + case head :: _ => + head.bindings = trailingBindings ::: head.bindings + Conjunction(clauses ::: suffix, Nil) + } + } + + /** + * This is a shorthand if you only have the last binding. + * + * @param suffix the list of clauses to append to this conjunction + * @return a new conjunction with clauses from `this` and `suffix` + */ + def +(lastBinding: (Bool, Var, Term)): Conjunction = + Conjunction(clauses, trailingBindings :+ lastBinding) + + def separate(expectedScrutinee: Scrutinee): Opt[(MatchClass, Conjunction)] = { + def rec(past: Ls[Clause], upcoming: Ls[Clause]): Opt[(Ls[Clause], MatchClass, Ls[Clause])] = { + upcoming match { + case Nil => N + case (head @ MatchClass(scrutinee, _, _)) :: tail => + if (scrutinee === expectedScrutinee) { + S((past, head, tail)) + } else { + rec(past :+ head, tail) + } + case head :: tail => + rec(past :+ head, tail) + } + } + + rec(Nil, clauses).map { case (past, wanted, remaining) => + (wanted, Conjunction(past ::: remaining, trailingBindings)) + } + } + + /** + * Prepend bindings to the first condition of this conjunction. + * + * @param interleavedLets the buffer of let bindings in the current context + * @return idential to `conditions` + */ + def withBindings(implicit interleavedLets: Buffer[(Bool, Var, Term)]): Conjunction = { + clauses match { + case Nil => Conjunction(Nil, interleavedLets.toList ::: trailingBindings) + case head :: _ => + head.bindings = head.bindings ::: interleavedLets.toList + this + } + } +} + +object Conjunction { + def empty: Conjunction = Conjunction(Nil, Nil) +} diff --git a/shared/src/main/scala/mlscript/ucs/Desugarer.scala b/shared/src/main/scala/mlscript/ucs/Desugarer.scala new file mode 100644 index 0000000000..0a8b7eac5f --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/Desugarer.scala @@ -0,0 +1,617 @@ +package mlscript.ucs + +import scala.collection.mutable.{Map => MutMap} +import scala.collection.mutable.Buffer + +import mlscript._, utils._, shorthands._ +import helpers._ + +/** + * This class contains main desugaring methods. + */ +class Desugarer extends TypeDefs { self: Typer => + var dbgUCS: Bool = false + private def printlnUCS(msg: => Any): Unit = if (dbgUCS) println(msg) + private def traceUCS[T](pre: => String)(thunk: => T)(post: T => String = noPostTrace) = + if (dbgUCS) trace(pre)(thunk)(post) else thunk + + import Clause.{MatchClass, MatchTuple, BooleanTest} + + type FieldAliasMap = MutMap[SimpleTerm, MutMap[Str, Var]] + + private var idLength: Int = 0 + + private def makeName: String = { + val name = s"tmp$idLength" + idLength += 1 + name + } + + private def freshName(implicit ctx: Ctx): String = { + var res = makeName + while (ctx.env.contains(res)) { + res = makeName + } + res + } + + /** + * + * + * @param scrutinee the scrutinee of the pattern matching + * @param params parameters provided by the + * @param positionals the corresponding field names of each parameter + * @param aliasMap a map used to cache each the alias of each field + * @param matchRootLoc the location to the root of the match + * @return a mapping from each field to their var + */ + private def desugarPositionals + (scrutinee: Scrutinee, params: IterableOnce[Term], positionals: Ls[Str]) + (implicit ctx: Ctx, aliasMap: FieldAliasMap): (Ls[Var -> Term], Ls[Str -> Var]) = { + val subPatterns = Buffer.empty[(Var, Term)] + val bindings = params.iterator.zip(positionals).flatMap { + // `x is A(_)`: ignore this binding + case (Var("_"), _) => N + // `x is A(value)`: generate bindings directly + case (name: Var, fieldName) => S(fieldName -> name) + // `x is B(A(x))`: generate a temporary name + // use the name in the binding, and destruct sub-patterns + case (pattern: Term, fieldName) => + // We should always use the same temporary for the same `fieldName`. + // This uniqueness is decided by (scrutinee, fieldName). + val alias = aliasMap + .getOrElseUpdate(scrutinee.reference, MutMap.empty) + .getOrElseUpdate(fieldName, Var(freshName).desugaredFrom(pattern)) + subPatterns += ((alias, pattern)) + S(fieldName -> alias) + }.toList + subPatterns.toList -> bindings + } + + /** + * Desugar sub-patterns from fields to conditions. + * + * @param subPatterns a list of field name -> pattern term + * @param ctx the typing context + * @param aliasMap the field alias map + * @return desugared conditions representing the sub-patterns + */ + private def destructSubPatterns(scrutinee: Scrutinee, subPatterns: Iterable[Var -> Term]) + (implicit ctx: Ctx, raise: Raise, aliasMap: FieldAliasMap): Ls[Clause] = { + subPatterns.iterator.flatMap[Clause] { case (subScrutinee, subPattern) => + destructPattern(makeScrutinee(subScrutinee, scrutinee.matchRootLoc), subPattern) + }.toList + } + + // `IdentityHashMap` is a workaround. + private val localizedScrutineeMap = new java.util.IdentityHashMap[Term, Var] + + /** + * Create a `Scrutinee`. If the `term` is a simple expression (e.g. `Var` or + * `Lit`), we do not create a local alias. Otherwise, we create a local alias + * to avoid unnecessary computations. + * + * @param term the term in the local scrutinee position + * @param matchRootLoc the caller is expect to be in a match environment, + * this parameter indicates the location of the match root + */ + def makeScrutinee(term: Term, matchRootLoc: Opt[Loc])(implicit ctx: Ctx): Scrutinee = + traceUCS(s"Making a scrutinee for `$term`") { + term match { + case _: SimpleTerm => Scrutinee(N, term)(matchRootLoc) + case _ => + val localName = if (localizedScrutineeMap.containsKey(term)) { + localizedScrutineeMap.get(term) + } else { + val v = Var(freshName).desugaredFrom(term) + localizedScrutineeMap.put(term, v) + v + } + Scrutinee(S(localName), term)(matchRootLoc) + } + }() + + /** + * Destruct nested patterns to a list of simple condition with bindings. + * + * @param scrutinee the scrutinee of the pattern matching + * @param pattern the pattern we will destruct + * @param raise the `Raise` function + * @param aliasMap the field alias map + * @param matchRootLoc the location of the root of the pattern matching + * @param fragments fragment term that used to construct the given pattern. + * It is used to tracking locations. + * @return a list of simple condition with bindings. This method does not + * return `ConjunctedCondition` because conditions built from nested patterns + * do not contain interleaved let bindings. + */ + private def destructPattern + (scrutinee: Scrutinee, pattern: Term) + (implicit ctx: Ctx, + raise: Raise, + aliasMap: FieldAliasMap, + fragments: Ls[Term] = Nil): Ls[Clause] = + trace(s"[Desugarer.destructPattern] scrutinee = ${scrutinee.term}; pattern = $pattern") { + // This piece of code is use in two match cases. + def desugarTuplePattern(tuple: Tup): Ls[Clause] = { + val (subPatterns, bindings) = desugarPositionals( + scrutinee, + tuple.fields.iterator.map(_._2.value), + 1.to(tuple.fields.length).map("_" + _).toList + ) + Clause.MatchTuple( + scrutinee, + tuple.fields.length, + bindings + )(collectLocations(scrutinee.term)) :: destructSubPatterns(scrutinee, subPatterns) + } + pattern match { + // This case handles top-level wildcard `Var`. + // We don't make any conditions in this level. + case Var("_") => Nil + // This case handles literals. + // x is true | x is false | x is 0 | x is "text" | ... + case literal @ (Var("true") | Var("false") | _: Lit) => + val test = mkBinOp(scrutinee.reference, Var("=="), literal) + val clause = Clause.BooleanTest(test)(scrutinee.term.toLoc.toList ::: literal.toLoc.toList) + clause.bindings = scrutinee.asBinding.toList + printlnUCS(s"Add bindings to the clause: ${scrutinee.asBinding}") + clause :: Nil + // This case handles simple class tests. + // x is A + case classNameVar @ Var(className) => + ctx.tyDefs.get(className) match { + case N => throw new DesugaringException({ + import Message.MessageContext + msg"Cannot find the constructor `$className` in the context" + }, classNameVar.toLoc) + case S(_) => + printlnUCS(s"Build a Clause.MatchClass from $scrutinee where pattern is $classNameVar") + Clause.MatchClass(scrutinee, classNameVar, Nil)(collectLocations(scrutinee.term)) :: Nil + } + // This case handles classes with destruction. + // x is A(r, s, t) + case app @ App(classNameVar @ Var(className), Tup(args)) => + ctx.tyDefs.get(className) match { + case N => + throw new DesugaringException({ + import Message.MessageContext + msg"Cannot find class `$className` in the context" + }, classNameVar.toLoc) + case S(td) => + if (args.length === td.positionals.length) { + val (subPatterns, bindings) = desugarPositionals( + scrutinee, + args.iterator.map(_._2.value), + td.positionals + ) + val clause = Clause.MatchClass(scrutinee, classNameVar, bindings)(pattern.toLoc.toList ::: collectLocations(scrutinee.term)) + printlnUCS(s"Build a Clause.MatchClass from $scrutinee where pattern is $pattern") + printlnUCS(s"Fragments: $fragments") + printlnUCS(s"The locations of the clause: ${clause.locations}") + clause :: destructSubPatterns(scrutinee, subPatterns) + } else { + throw new DesugaringException({ + import Message.MessageContext + val expected = td.positionals.length + val actual = args.length + msg"${td.kind.str} $className expects ${expected.toString} ${ + "parameter".pluralize(expected) + } but found ${args.length.toString} ${ + "parameter".pluralize(expected) + }" + }, app.toLoc) + } + } + // This case handles operator-like constructors. + // x is head :: Nil + case app @ App( + App( + opVar @ Var(op), + Tup((_ -> Fld(_, _, lhs)) :: Nil) + ), + Tup((_ -> Fld(_, _, rhs)) :: Nil) + ) => + ctx.tyDefs.get(op) match { + case N => + throw new DesugaringException({ + import Message.MessageContext + msg"Cannot find operator `$op` in the context" + }, opVar.toLoc) + case S(td) if td.positionals.length === 2 => + val (subPatterns, bindings) = desugarPositionals( + scrutinee, + lhs :: rhs :: Nil, + td.positionals + ) + val clause = Clause.MatchClass(scrutinee, opVar, bindings)(collectLocations(scrutinee.term)) + printlnUCS(s"Build a Clause.MatchClass from $scrutinee where operator is $opVar") + clause :: destructSubPatterns(scrutinee, subPatterns) + case S(td) => + val num = td.positionals.length + throw new DesugaringException({ + import Message.MessageContext + val expected = td.positionals.length + msg"${td.kind.str} `$op` expects ${expected.toString} ${ + "parameter".pluralize(expected) + } but found two parameters" + }, app.toLoc) + } + // This case handles **direct** tuple destructions. + // x is (a, b, c) + case Bra(_, tuple: Tup) => desugarTuplePattern(tuple) + // This case handles **nested** tuple destructions. + // x is Cons((x, y), Nil) + case tuple: Tup => desugarTuplePattern(tuple) + // What else? + case _ => throw new Exception(s"illegal pattern: ${mlscript.codegen.Helpers.inspect(pattern)}") + } + }("[Desugarer.destructPattern] result: " + Clause.showClauses(_)) + + /** + * Collect `Loc`s from a synthetic term. + * + * @param term the root of the synthetic term + * @param fragments the fragment terms + * @return all original locations + */ + def collectLocations(term: Term)(implicit fragments: Ls[Term]): Ls[Loc] = { + val locations = Buffer.empty[Loc] + def rec(term: Term): Unit = term.children.foreach { located => + if (fragments.contains(located)) locations ++= located.toLoc + } + locations.toList + } + + + def desugarIf + (body: IfBody, fallback: Opt[Term]) + (implicit ctx: Ctx, raise: Raise) + : Ls[Conjunction -> Term] = { + // We allocate temporary variable names for nested patterns. + // This prevents aliasing problems. + implicit val scrutineeFieldAliasMap: FieldAliasMap = MutMap.empty + // A list of flattened if-branches. + val branches = Buffer.empty[Conjunction -> Term] + + /** + * Translate a list of atomic UCS conditions. + * What is atomic? No "and". + * + * @param ts a list of atomic UCS conditions + * @return a list of `Condition` + */ + def desugarConditions(ts: Ls[Term])(implicit fragments: Ls[Term] = Nil): Ls[Clause] = + ts.flatMap { + case isApp @ App( + App(Var("is"), + Tup(_ -> Fld(_, _, scrutinee) :: Nil)), + Tup(_ -> Fld(_, _, pattern) :: Nil) + ) => + // This is an inline `x is Class` match test. + val inlineMatchLoc = isApp.toLoc + val inlineScrutinee = makeScrutinee(scrutinee, inlineMatchLoc) + destructPattern(inlineScrutinee, pattern)(ctx, raise, scrutineeFieldAliasMap) + case test => + val clause = Clause.BooleanTest(test)(collectLocations(test)) + Iterable.single(clause) + } + + /** + * Recursively desugar a pattern matching branch. + * + * @param scrutinee the scrutinee of this pattern matching + * @param body one element of `lines` of the `IfBlock` + * @param pat the accumulated pattern, since patterns can be split + * @param acc the accumulated conditions so far + * @param ctx the typing context + * @param interleavedLets interleaved let bindings before this branch + * @param rootLoc the location of the `IfOpApp` + */ + def desugarMatchBranch( + scrutinee: Scrutinee, + body: IfBody \/ Statement, + partialPattern: PartialTerm, + collectedConditions: Conjunction, + )(implicit interleavedLets: Buffer[(Bool, Var, Term)]): Unit = + body match { + // This case handles default branches. For example, + // if x is + // A(...) then ... + // else ... + case L(IfElse(consequent)) => + // Because this pattern matching is incomplete, it's not included in + // `acc`. This means that we discard this incomplete pattern matching. + branches += (collectedConditions -> consequent) + // This case handles default branches indicated by wildcards. + // if x is + // A(...) then ... + // _ then ... + case L(IfThen(Var("_"), consequent)) => + branches += (collectedConditions -> consequent) + // if x is + // A(...) then ... // Case 1: no conjunctions + // B(...) and ... then ... // Case 2: more conjunctions + case L(IfThen(patTest, consequent)) => + val (patternPart, extraTestOpt) = separatePattern(patTest) + val clauses = destructPattern(scrutinee, partialPattern.addTerm(patternPart).term) + val conditions = collectedConditions + Conjunction(clauses, Nil).withBindings + printlnUCS(s"result conditions: " + Clause.showClauses(conditions.clauses)) + extraTestOpt match { + // Case 1. Just a pattern. Easy! + case N => + branches += (conditions -> consequent) + // Case 2. A pattern and an extra test + case S(extraTest) => + desugarIfBody(IfThen(extraTest, consequent), PartialTerm.Empty, conditions) + } + // if x is + // A(...) and t <> // => IfOpApp(A(...), "and", IfOpApp(...)) + // a then ... + // b then ... + // A(...) and y is // => IfOpApp(A(...), "and", IfOpApp(...)) + // B(...) then ... + // B(...) then ... + case L(IfOpApp(patLhs, Var("and"), consequent)) => + val (pattern, optTests) = separatePattern(patLhs) + val patternConditions = destructPattern(scrutinee, pattern) + val tailTestConditions = optTests.fold(Nil: Ls[Clause])(x => desugarConditions(splitAnd(x))) + val conditions = + collectedConditions + Conjunction(patternConditions ::: tailTestConditions, Nil).withBindings + desugarIfBody(consequent, PartialTerm.Empty, conditions) + case L(IfOpApp(patLhs, op, consequent)) => + separatePattern(patLhs) match { + // Case 1. + // The pattern is completed. There is also a conjunction. + // So, we need to separate the pattern from remaining parts. + case (pattern, S(extraTests)) => + val patternConditions = destructPattern(scrutinee, pattern) + val extraConditions = desugarConditions(splitAnd(extraTests)) + val conditions = + collectedConditions + Conjunction(patternConditions ::: extraConditions, Nil).withBindings + desugarIfBody(consequent, PartialTerm.Empty, conditions) + // Case 2. + // The pattern is incomplete. Remaining parts are at next lines. + // if x is + // head :: + // Nil then ... // do something with head + // tail then ... // do something with head and tail + case (patternPart, N) => + desugarMatchBranch(scrutinee, L(consequent), partialPattern.addTermOp(patternPart, op), collectedConditions) + } + case L(IfOpsApp(patLhs, opsRhss)) => + separatePattern(patLhs) match { + case (patternPart, N) => + val partialPattern2 = partialPattern.addTerm(patternPart) + opsRhss.foreach { case op -> consequent => + desugarMatchBranch(scrutinee, L(consequent), partialPattern2.addOp(op), collectedConditions) + } + case (patternPart, S(extraTests)) => + val patternConditions = destructPattern(scrutinee, partialPattern.addTerm(patternPart).term) + val testTerms = splitAnd(extraTests) + val middleConditions = desugarConditions(testTerms.init) + val conditions = + collectedConditions + Conjunction(patternConditions ::: middleConditions, Nil).withBindings + opsRhss.foreach { case op -> consequent => + // TODO: Use lastOption + val last = testTerms.last + val partialTerm = PartialTerm.Total(last, last :: Nil) + desugarIfBody(consequent, partialTerm, conditions) + } + } + // This case usually happens with pattern split by linefeed. + case L(IfBlock(lines)) => + lines.foreach { desugarMatchBranch(scrutinee, _, partialPattern, collectedConditions) } + // This case is rare. Let's put it aside. + case L(IfLet(_, _, _, _)) => + TODO("please add this rare case to test files") + // This case handles interleaved lets. + case R(NuFunDef(S(isRec), nameVar, _, L(term))) => + interleavedLets += ((isRec, nameVar, term)) + // Other statements are considered to be ill-formed. + case R(statement) => throw new DesugaringException({ + import Message.MessageContext + msg"Illegal interleaved statement ${statement.toString}" + }, statement.toLoc) + } + def desugarIfBody + (body: IfBody, expr: PartialTerm, acc: Conjunction) + (implicit interleavedLets: Buffer[(Bool, Var, Term)]) + : Unit = { + body match { + case IfOpsApp(exprPart, opsRhss) => + val exprStart = expr.addTerm(exprPart) + opsRhss.foreach { case opVar -> contBody => + desugarIfBody(contBody, exprStart.addOp(opVar), acc) + } + case IfThen(Var("_"), consequent) => + branches += (acc -> consequent) + // The termination case. + case IfThen(term, consequent) => + val totalTerm = expr.addTerm(term) + // “Atomic” means terms that do not contain `and`. + val atomicTerms = splitAnd(totalTerm.term) + val fragments = atomicTerms ::: totalTerm.fragments + val newClauses = desugarConditions(atomicTerms)(fragments) + branches += ((acc + newClauses).withBindings -> consequent) + // This is the entrance of the Simple UCS. + case IfOpApp(scrutineePart, isVar @ Var("is"), IfBlock(lines)) => + // Create a synthetic scrutinee term by combining accumulated partial + // term with the new part. + val scrutineeTerm = expr.addTerm(scrutineePart).term + // We don't need to include the entire `IfOpApp` because it might be + // very long... Indicating the beginning of the match is enough. + val matchRootLoc = (scrutineeTerm.toLoc, isVar.toLoc) match { + case (S(first), S(second)) => S(first ++ second) + case (_, _) => N + } + val scrutinee = makeScrutinee(scrutineeTerm, matchRootLoc) + // If there is an alias, we should add the let bindings to clauses. + val conjunction = scrutinee.local match { + case S(alias) => acc + case N => acc + } + // Create a buffer for interleaved let bindings. + val interleavedLets = Buffer.empty[(Bool, Var, Term)] + // Iterate each match case. + lines.foreach { + desugarMatchBranch(scrutinee, _, PartialTerm.Empty, conjunction)(interleavedLets) + } + // For example: "if x == 0 and y is \n ..." + case IfOpApp(testPart, Var("and"), consequent) => + val conditions = acc + (desugarConditions(expr.addTerm(testPart).term :: Nil)) + desugarIfBody(consequent, PartialTerm.Empty, conditions) + // Otherwise, this is not a pattern matching. + // We create a partial term from `lhs` and `op` and dive deeper. + case IfOpApp(lhs, op, body) => + desugarIfBody(body, expr.addTermOp(lhs, op), acc) + // This case is rare. Let's put it aside. + case IfLet(isRec, name, rhs, body) => + TODO("please add this rare case to test files") + // In this case, the accumulated partial term is discarded. + // We create a branch directly from accumulated conditions. + case IfElse(term) => branches += (acc.withBindings -> term) + case IfBlock(lines) => + lines.foreach { + case L(subBody) => desugarIfBody(subBody, expr, acc) + case R(NuFunDef(S(isRec), nameVar, _, L(term))) => + interleavedLets += ((isRec, nameVar, term)) + case R(_) => + throw new Error("unexpected statements at desugarIfBody") + } + } + } + // Top-level interleaved let bindings. + val interleavedLets = Buffer.empty[(Bool, Var, Term)] + desugarIfBody(body, PartialTerm.Empty, Conjunction.empty)(interleavedLets) + // Add the fallback case to conjunctions if there is any. + fallback.foreach { branches += Conjunction.empty -> _ } + Clause.print(printlnUCS, branches) + branches.toList + } + + import MutCaseOf.{MutCase, IfThenElse, Match, MissingCase, Consequent} + + /** + * A map from each scrutinee term to all its cases and the first `MutCase`. + */ + type ExhaustivenessMap = Map[Str \/ Int, Map[Var, MutCase]] + + def getScurtineeKey(scrutinee: Scrutinee)(implicit ctx: Ctx, raise: Raise): Str \/ Int = { + scrutinee.term match { + // The original scrutinee is an reference. + case v @ Var(name) => + ctx.env.get(name) match { + case S(VarSymbol(_, defVar)) => defVar.uid.fold[Str \/ Int](L(v.name))(R(_)) + case S(_) | N => L(v.name) + } + // Otherwise, the scrutinee has a temporary name. + case _ => + scrutinee.local match { + case N => throw new Error("check your `makeScrutinee`") + case S(localNameVar) => L(localNameVar.name) + } + } + } + + /** + * Check the exhaustiveness of the given `MutCaseOf`. + * + * @param t the mutable `CaseOf` of + * @param parentOpt the parent `MutCaseOf` + * @param scrutineePatternMap the exhaustiveness map + */ + def checkExhaustive + (t: MutCaseOf, parentOpt: Opt[MutCaseOf]) + (implicit scrutineePatternMap: ExhaustivenessMap, ctx: Ctx, raise: Raise) + : Unit = { + printlnUCS(s"Check exhaustiveness of ${t.describe}") + indent += 1 + try t match { + case _: Consequent => () + case MissingCase => + import Message.MessageContext + parentOpt match { + case S(IfThenElse(test, whenTrue, whenFalse)) => + if (whenFalse === t) + throw new DesugaringException(msg"The case when this is false is not handled: ${test.toString}", test.toLoc) + else + lastWords("`MissingCase` are not supposed to be the true branch of `IfThenElse`") + case S(Match(_, _, _)) => + lastWords("`MissingCase` are not supposed to be a case of `Match`") + case S(Consequent(_)) | S(MissingCase) | N => die // unreachable + } + case IfThenElse(condition, whenTrue, whenFalse) => + checkExhaustive(whenTrue, S(t)) + checkExhaustive(whenFalse, S(t)) + case Match(scrutinee, branches, default) => + scrutineePatternMap.get(getScurtineeKey(scrutinee)) match { + case N => lastWords(s"unreachable case: unknown scrutinee ${scrutinee.term}") + case S(patternMap) => + printlnUCS(s"The exhaustiveness map is ${scrutineePatternMap}") + printlnUCS(s"The scrutinee key is ${getScurtineeKey(scrutinee)}") + printlnUCS("Pattern map of the scrutinee:") + if (patternMap.isEmpty) + printlnUCS("") + else + patternMap.foreach { case (key, mutCase) => printlnUCS(s"- $key => $mutCase")} + // Filter out missing cases in `branches`. + val missingCases = patternMap.removedAll(branches.iterator.map { + case MutCase(classNameVar -> _, _) => classNameVar + }) + printlnUCS(s"Number of missing cases: ${missingCases.size}") + if (!missingCases.isEmpty) { + throw new DesugaringException({ + import Message.MessageContext + val numMissingCases = missingCases.size + (msg"The match is not exhaustive." -> scrutinee.matchRootLoc) :: + (msg"The scrutinee at this position misses ${numMissingCases.toString} ${ + "case".pluralize(numMissingCases) + }." -> scrutinee.term.toLoc) :: + missingCases.iterator.zipWithIndex.flatMap { case ((classNameVar, firstMutCase), index) => + val progress = s"[Missing Case ${index + 1}/$numMissingCases]" + (msg"$progress `${classNameVar.name}`" -> N) :: + firstMutCase.locations.iterator.zipWithIndex.map { case (loc, index) => + (if (index === 0) msg"It first appears here." else msg"continued at") -> S(loc) + }.toList + }.toList + }) + } + } + default.foreach(checkExhaustive(_, S(t))) + branches.foreach { case MutCase(_, consequent) => + checkExhaustive(consequent, S(t)) + } + } finally indent -= 1 + } + + def summarizePatterns(t: MutCaseOf)(implicit ctx: Ctx, raise: Raise): ExhaustivenessMap = { + val m = MutMap.empty[Str \/ Int, MutMap[Var, MutCase]] + def rec(t: MutCaseOf): Unit = { + printlnUCS(s"Summarize pattern of ${t.describe}") + indent += 1 + try t match { + case Consequent(term) => () + case MissingCase => () + case IfThenElse(_, whenTrue, whenFalse) => + rec(whenTrue) + rec(whenFalse) + case Match(scrutinee, branches, _) => + val key = getScurtineeKey(scrutinee) + branches.foreach { mutCase => + val patternMap = m.getOrElseUpdate( key, MutMap.empty) + if (!patternMap.contains(mutCase.patternFields._1)) { + patternMap += ((mutCase.patternFields._1, mutCase)) + } + rec(mutCase.consequent) + } + } finally indent -= 1 + } + rec(t) + printlnUCS("Exhaustiveness map") + m.foreach { case (scrutinee, patterns) => + printlnUCS(s"- $scrutinee => " + patterns.keys.mkString(", ")) + } + Map.from(m.iterator.map { case (key, patternMap) => key -> Map.from(patternMap) }) + } +} diff --git a/shared/src/main/scala/mlscript/ucs/DesugaringException.scala b/shared/src/main/scala/mlscript/ucs/DesugaringException.scala new file mode 100644 index 0000000000..1e99cdccea --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/DesugaringException.scala @@ -0,0 +1,8 @@ +package mlscript.ucs + +import mlscript.{Diagnostic, Loc, Message, Typer} +import mlscript.utils.shorthands._ + +class DesugaringException(val messages: Ls[Message -> Opt[Loc]]) extends Throwable { + def this(message: Message, location: Opt[Loc]) = this(message -> location :: Nil) +} diff --git a/shared/src/main/scala/mlscript/ucs/MutCaseOf.scala b/shared/src/main/scala/mlscript/ucs/MutCaseOf.scala new file mode 100644 index 0000000000..37d9084999 --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/MutCaseOf.scala @@ -0,0 +1,368 @@ +package mlscript.ucs + +import mlscript._ +import mlscript.utils._ +import mlscript.utils.shorthands._ +import scala.collection.immutable.Set +import scala.collection.mutable.{Map => MutMap, Set => MutSet, Buffer} + +import helpers._ +import mlscript.ucs.MutCaseOf.Consequent + +trait WithBindings { this: MutCaseOf => + private val bindingsSet: MutSet[(Bool, Var, Term)] = MutSet.empty + private val bindings: Buffer[(Bool, Var, Term)] = Buffer.empty + + def addBindings(newBindings: IterableOnce[(Bool, Var, Term)]): Unit = { + newBindings.iterator.foreach { + case binding if bindingsSet.contains(binding) => () + case binding => + bindingsSet += binding + bindings += binding + } + } + + def getBindings: Iterable[(Bool, Var, Term)] = bindings + + def withBindings(newBindings: IterableOnce[(Bool, Var, Term)]): MutCaseOf = { + addBindings(newBindings) + this + } +} + +sealed abstract class MutCaseOf extends WithBindings { + def kind: Str = { + import MutCaseOf._ + this match { + case Consequent(_) => "Consequent" + case MissingCase => "MissingCase" + case IfThenElse(_, _, _) => "IfThenElse" + case Match(_, _, _) => "Match" + } + } + + def describe: Str + + def merge + (branch: Conjunction -> Term) + (implicit raise: Diagnostic => Unit): Unit + def mergeDefault + (bindings: Ls[(Bool, Var, Term)], default: Term) + (implicit raise: Diagnostic => Unit): Unit + def toTerm(defs: Set[Var]): Term + + // TODO: Make it immutable. + var locations: Ls[Loc] = Nil +} + +object MutCaseOf { + def toTerm(t: MutCaseOf): Term = { + val term = t.toTerm(Set.from(t.getBindings.iterator.map(_._2))) + mkBindings(t.getBindings.toList, term, Set.empty) + } + + def showScrutinee(scrutinee: Scrutinee): Str = + s"«${scrutinee.term}»" + (scrutinee.local match { + case N => "" + case S(Var(alias)) => s" as $alias" + }) + + def show(t: MutCaseOf): Ls[Str] = { + val lines = Buffer.empty[String] + def rec(t: MutCaseOf, indent: Int, leading: String): Unit = { + val baseIndent = " " * indent + val bindingNames = t.getBindings match { + case Nil => "" + case bindings => bindings.iterator.map(_._2.name).mkString("[", ", ", "] ") + } + t match { + case IfThenElse(condition, whenTrue, whenFalse) => + // Output the `whenTrue` with the prefix "if". + lines += baseIndent + leading + bindingNames + s"if «$condition»" + rec(whenTrue, indent + 1, "") + // Output the `whenFalse` case with the prefix "else". + lines += s"$baseIndent${leading}else" + rec(whenFalse, indent + 1, "") + case Match(scrutinee, branches, default) => + lines += baseIndent + leading + bindingNames + showScrutinee(scrutinee) + " match" + branches.foreach { case MutCase(Var(className) -> fields, consequent) => + lines += s"$baseIndent case $className =>" + fields.foreach { case (field, Var(alias)) => + lines += s"$baseIndent let $alias = .$field" + } + rec(consequent, indent + 2, "") + } + default.foreach { consequent => + lines += s"$baseIndent default" + rec(consequent, indent + 2, "") + } + case Consequent(term) => + lines += s"$baseIndent$leading$bindingNames«$term»" + case MissingCase => + lines += s"$baseIndent$leading$bindingNames" + } + } + rec(t, 0, "") + lines.toList + } + + /** + * MutCase is a _mutable_ representation of a case in `MutCaseOf.Match`. + * + * @param patternFields the alias to the fields + * @param consequent the consequential `MutCaseOf` + */ + final case class MutCase( + val patternFields: Var -> Buffer[Str -> Var], + var consequent: MutCaseOf, + ) { + def matches(expected: Var): Bool = matches(expected.name) + def matches(expected: Str): Bool = patternFields._1.name === expected + def addFields(fields: Iterable[Str -> Var]): Unit = + patternFields._2 ++= fields.iterator.filter(!patternFields._2.contains(_)) + + // Note 1 + // ====== + // A `MutCase` may come from one of two origins. + // Direct patterns. + // E.g. if x is Y then "aha" else "meh" + // ^^^^^^ + // Nested patterns. + // E.g. if x is Right(Some(x)) then ... + // ^^^^^^^ + // The goal is to accurately indicate where the pattern is declared. + // + // Note 2 + // ====== + // A `MutCase` may come from multiple locations. + // That is why I'm using a `Set`. + // + val locations: MutSet[Loc] = MutSet.empty[Loc] + def withLocation(locOpt: Opt[Loc]): MutCase = { + locations ++= locOpt + this + } + def withLocations(locs: Ls[Loc]): MutCase = { + locations ++= locs + this + } + } + + import Clause.{MatchClass, MatchTuple, BooleanTest} + + // A short-hand for pattern matchings with only true and false branches. + final case class IfThenElse(condition: Term, var whenTrue: MutCaseOf, var whenFalse: MutCaseOf) extends MutCaseOf { + def describe: Str = + s"IfThenElse($condition, whenTrue = ${whenTrue.kind}, whenFalse = ${whenFalse.kind})" + + def merge(branch: Conjunction -> Term)(implicit raise: Diagnostic => Unit): Unit = + branch match { + // The CC is a wildcard. So, we call `mergeDefault`. + case Conjunction(Nil, trailingBindings) -> term => + this.mergeDefault(trailingBindings, term) + // The CC is an if-then-else. We create a pattern match of true/false. + case Conjunction((head @ BooleanTest(test)) :: tail, trailingBindings) -> term => + // If the test is the same. So, we merge. + if (test === condition) { + whenTrue.addBindings(head.bindings) + whenTrue.merge(Conjunction(tail, trailingBindings) -> term) + } else { + whenFalse match { + case Consequent(_) => + raise(WarningReport(Message.fromStr("duplicated else in the if-then-else") -> N :: Nil)) + case MissingCase => + whenFalse = buildFirst(branch._1, branch._2) + whenFalse.addBindings(head.bindings) + case _ => whenFalse.merge(branch) + } + } + case Conjunction(head :: _, _) -> _ => + whenFalse match { + case Consequent(_) => + raise(WarningReport(Message.fromStr("duplicated else in the if-then-else") -> N :: Nil)) + case MissingCase => + whenFalse = buildFirst(branch._1, branch._2) + whenFalse.addBindings(head.bindings) + case _ => whenFalse.merge(branch) + } + } + + def mergeDefault(bindings: Ls[(Bool, Var, Term)], default: Term)(implicit raise: Diagnostic => Unit): Unit = { + whenTrue.mergeDefault(bindings, default) + whenFalse match { + case Consequent(term) => + import Message.MessageContext + raise(WarningReport( + msg"Found a duplicated else branch" -> default.toLoc :: + (msg"The first else branch was declared here." -> term.toLoc) :: + Nil)) + case MissingCase => + whenFalse = Consequent(default).withBindings(bindings) + case _: IfThenElse | _: Match => whenFalse.mergeDefault(bindings, default) + } + } + + def toTerm(defs: Set[Var]): Term = { + val falseBody = mkBindings(whenFalse.getBindings.toList, whenFalse.toTerm(defs ++ whenFalse.getBindings.iterator.map(_._2)), defs) + val trueBody = mkBindings(whenTrue.getBindings.toList, whenTrue.toTerm(defs ++ whenTrue.getBindings.iterator.map(_._2)), defs) + val falseBranch = Wildcard(falseBody) + val trueBranch = Case(Var("true"), trueBody, falseBranch) + CaseOf(condition, trueBranch) + } + } + final case class Match( + scrutinee: Scrutinee, + val branches: Buffer[MutCase], + var wildcard: Opt[MutCaseOf] + ) extends MutCaseOf { + def describe: Str = { + val n = branches.length + s"Match($scrutinee, $n ${"branch".pluralize(n, true)}, ${ + wildcard.fold("no wildcard")(n => s"wildcard = ${n.kind}") + })" + } + + def merge(branch: Conjunction -> Term)(implicit raise: Diagnostic => Unit): Unit = { + branch._1.separate(scrutinee) match { + // No conditions against the same scrutinee. + case N => + branch match { + case Conjunction((head @ MatchTuple(scrutinee2, arity, fields)) :: tail, trailingBindings) -> term + if scrutinee2 === scrutinee => // Same scrutinee! + val tupleClassName = Var(s"Tuple#$arity") // TODO: Find a name known by Typer. + branches.find(_.matches(tupleClassName)) match { + // No such pattern. We should create a new one. + case N => + val newBranch = buildFirst(Conjunction(tail, trailingBindings), term) + newBranch.addBindings(head.bindings) + branches += MutCase(tupleClassName -> Buffer.from(fields), newBranch) + .withLocations(head.locations) + // Found existing pattern. + case S(branch) => + branch.consequent.addBindings(head.bindings) + branch.addFields(fields) + branch.consequent.merge(Conjunction(tail, trailingBindings) -> term) + } + // A wild card case. We should propagate wildcard to every default positions. + case Conjunction(Nil, trailingBindings) -> term => mergeDefault(trailingBindings, term) + // The conditions to be inserted does not overlap with me. + case conjunction -> term => + wildcard match { + // No wildcard. We will create a new one. + case N => wildcard = S(buildFirst(conjunction, term)) + // There is a wildcard case. Just merge! + case S(consequent) => consequent.merge(conjunction -> term) + } + } + // Found a match condition against the same scrutinee + case S((head @ MatchClass(_, className, fields), remainingConditions)) => + branches.find(_.matches(className)) match { + // No such pattern. We should create a new one. + case N => + val newBranch = buildFirst(remainingConditions, branch._2) + newBranch.addBindings(head.bindings) + branches += MutCase(className -> Buffer.from(fields), newBranch) + .withLocations(head.locations) + // Found existing pattern. + case S(matchCase) => + // Merge interleaved bindings. + matchCase.consequent.addBindings(head.bindings) + matchCase.addFields(fields) + matchCase.consequent.merge(remainingConditions -> branch._2) + } + } + } + + def mergeDefault(bindings: Ls[(Bool, Var, Term)], default: Term)(implicit raise: Diagnostic => Unit): Unit = { + branches.foreach { + case MutCase(_, consequent) => consequent.mergeDefault(bindings, default) + } + wildcard match { + case N => wildcard = S(Consequent(default).withBindings(bindings)) + case S(consequent) => consequent.mergeDefault(bindings, default) + } + } + + def toTerm(defs: Set[Var]): Term = { + def rec(xs: Ls[MutCase]): CaseBranches = + xs match { + case MutCase(className -> fields, cases) :: next => + // TODO: expand bindings here + val consequent = cases.toTerm(defs ++ fields.iterator.map(_._2)) + Case(className, mkLetFromFields(scrutinee, fields.toList, consequent), rec(next)) + case Nil => + wildcard.fold[CaseBranches](NoCases)(_.toTerm(defs) |> Wildcard) + } + val cases = rec(branches.toList) + val resultTerm = scrutinee.local match { + case N => CaseOf(scrutinee.term, cases) + case S(aliasVar) => Let(false, aliasVar, scrutinee.term, CaseOf(aliasVar, cases)) + } + // Collect let bindings from case branches. + val bindings = branches.iterator.flatMap(_.consequent.getBindings).toList + mkBindings(bindings, resultTerm, defs) + } + } + final case class Consequent(term: Term) extends MutCaseOf { + def describe: Str = s"Consequent($term)" + + def merge(branch: Conjunction -> Term)(implicit raise: Diagnostic => Unit): Unit = + raise(WarningReport(Message.fromStr("duplicated branch") -> N :: Nil)) + + def mergeDefault(bindings: Ls[(Bool, Var, Term)], default: Term)(implicit raise: Diagnostic => Unit): Unit = () + + def toTerm(defs: Set[Var]): Term = term + } + final case object MissingCase extends MutCaseOf { + def describe: Str = "MissingCase" + + def merge(branch: Conjunction -> Term)(implicit raise: Diagnostic => Unit): Unit = + lastWords("`MissingCase` is a placeholder and cannot be merged") + + def mergeDefault(bindings: Ls[(Bool, Var, Term)], default: Term)(implicit raise: Diagnostic => Unit): Unit = () + + def toTerm(defs: Set[Var]): Term = { + import Message.MessageContext + throw new DesugaringException(msg"missing a default branch", N) + } + } + + private def buildFirst(conjunction: Conjunction, term: Term): MutCaseOf = { + def rec(conjunction: Conjunction): MutCaseOf = conjunction match { + case Conjunction(head :: tail, trailingBindings) => + val realTail = Conjunction(tail, trailingBindings) + (head match { + case BooleanTest(test) => IfThenElse(test, rec(realTail), MissingCase) + case MatchClass(scrutinee, className, fields) => + val branches = Buffer( + MutCase(className -> Buffer.from(fields), rec(realTail)) + .withLocations(head.locations) + ) + Match(scrutinee, branches, N) + case MatchTuple(scrutinee, arity, fields) => + val branches = Buffer( + MutCase(Var(s"Tuple#$arity") -> Buffer.from(fields), rec(realTail)) + .withLocations(head.locations) + ) + Match(scrutinee, branches, N) + }).withBindings(head.bindings) + case Conjunction(Nil, trailingBindings) => + Consequent(term).withBindings(trailingBindings) + } + + rec(conjunction) + } + + def build + (cnf: Ls[Conjunction -> Term]) + (implicit raise: Diagnostic => Unit) + : MutCaseOf = { + cnf match { + case Nil => MissingCase + case (conditions -> term) :: next => + val root = MutCaseOf.buildFirst(conditions, term) + next.foreach(root.merge(_)) + root + } + } +} diff --git a/shared/src/main/scala/mlscript/ucs/PartialTerm.scala b/shared/src/main/scala/mlscript/ucs/PartialTerm.scala new file mode 100644 index 0000000000..3726d7a56a --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/PartialTerm.scala @@ -0,0 +1,50 @@ +package mlscript.ucs + +import mlscript._ +import mlscript.utils.shorthands._ + +import helpers._ + +class PartialTermError(term: PartialTerm, message: Str) extends Error(message) + +/** + * A `PartialTerm` represents a possibly incomplete term. + * We'd better precisely track detailed locations of each parts. + * + * @param fragments fragment terms that used to build this `PartialTerm`. + */ +sealed abstract class PartialTerm { + val fragments: Ls[Term] + def addTerm(term: Term): PartialTerm.Total + def addOp(op: Var): PartialTerm.Half + def addTermOp(term: Term, op: Var): PartialTerm.Half = + this.addTerm(term).addOp(op) +} + +object PartialTerm { + final case object Empty extends PartialTerm { + val fragments: Ls[Term] = Nil + def addTerm(term: Term): Total = Total(term, term :: Nil) + def addOp(op: Var): Half = + throw new PartialTermError(this, s"expect a term but operator $op was given") + } + + final case class Total(term: Term, fragments: Ls[Term]) extends PartialTerm { + def addTerm(term: Term): Total = + throw new PartialTermError(this, s"expect an operator but term $term was given") + def addOp(op: Var): Half = Half(term, op, op :: fragments) + } + + final case class Half(lhs: Term, op: Var, fragments: Ls[Term]) extends PartialTerm { + def addTerm(rhs: Term): Total = { + val (realRhs, extraExprOpt) = separatePattern(rhs) + val leftmost = mkBinOp(lhs, op, realRhs) + extraExprOpt match { + case N => Total(leftmost, fragments) + case S(extraExpr) => Total(mkBinOp(leftmost, Var("and"), extraExpr), extraExpr :: fragments) + } + } + def addOp(op: Var): Half = + throw new PartialTermError(this, s"expect a term but operator $op was given") + } +} diff --git a/shared/src/main/scala/mlscript/ucs/Scrutinee.scala b/shared/src/main/scala/mlscript/ucs/Scrutinee.scala new file mode 100644 index 0000000000..48a447ee51 --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/Scrutinee.scala @@ -0,0 +1,28 @@ +package mlscript.ucs + +import mlscript.{Loc, SimpleTerm, Term, Var} +import mlscript.utils.lastWords +import mlscript.utils.shorthands._ + +// The point is to remember where the scrutinee comes from. +// Is it from nested patterns? Or is it from a `IfBody`? +final case class Scrutinee(local: Opt[Var], term: Term)(val matchRootLoc: Opt[Loc]) { + def reference: SimpleTerm = local.getOrElse(term match { + case term: SimpleTerm => term + case _ => lastWords("`term` must be a `SimpleTerm` when `local` is empty") + }) + + /** + * Create a binding for the scrutinee. If the scrutinee is a `SimpleTerm`, + * it returns `None`. + * + * @return `Some` if the scrutinee is localized, otherwise, `None`. + */ + def asBinding: Opt[(Bool, Var, Term)] = local.map((false, _, term)) + + override def toString: String = + (local match { + case N => "" + case S(Var(alias)) => s"$alias @ " + }) + s"$term" +} diff --git a/shared/src/main/scala/mlscript/ucs/helpers.scala b/shared/src/main/scala/mlscript/ucs/helpers.scala new file mode 100644 index 0000000000..0b20c070fb --- /dev/null +++ b/shared/src/main/scala/mlscript/ucs/helpers.scala @@ -0,0 +1,117 @@ +package mlscript.ucs + +import scala.collection.mutable.{Set => MutSet} + +import mlscript._ +import mlscript.utils.shorthands._ + +object helpers { + /** + * Make a tuple with only one element. For example, + * + * ```scala + * mkMonuple(t) = Tup(N -> Fld(false, false, t) :: Nil) + * ``` + * + * @param t the sole element + * @return a tuple term with the only element + */ + def mkMonuple(t: Term): Tup = Tup(N -> Fld(false, false, t) :: Nil) + + /** + * Make a binary operation. + * + * @param lhs the left-hand side term + * @param op the operator + * @param rhs the right-hand side term + * @return something like `App(App(op, lhs), rhs)` + */ + def mkBinOp(lhs: Term, op: Var, rhs: Term): Term = + App(App(op, mkMonuple(lhs)), mkMonuple(rhs)) + + /** + * Split a term joined by `and` into a list of terms. + * E.g. `x and y and z` will be split into `x`, `y`, and `z`. + * + * @return a list of sub-terms of `t` + */ + def splitAnd(t: Term): Ls[Term] = + t match { + case App( + App(Var("and"), + Tup((_ -> Fld(_, _, lhs)) :: Nil)), + Tup((_ -> Fld(_, _, rhs)) :: Nil) + ) => + splitAnd(lhs) :+ rhs + case _ => t :: Nil + } + + /** + * Split a term into two parts: the pattern and the extra test. + * This is used to extract patterns from UCS conjunctions. For example, + * the second line results in `IfThen(Some(xv) and xv == 0, ...)` + * in the following case. + * + * ``` + * if x is + * Some(xv) and xv == 0 then ... + * ``` + * + * We must separate `Some(xv)` from the term to complete the pattern + * `x is Some(xv)`. + * + * @param term a term which might contains a pattern and an extra test + * @return a tuple, whose the first element is the pattern and the second + * element is the extra test + */ + def separatePattern(term: Term): (Term, Opt[Term]) = + term match { + case App( + App(and @ Var("and"), + Tup((_ -> Fld(_, _, lhs)) :: Nil)), + Tup((_ -> Fld(_, _, rhs)) :: Nil) + ) => + separatePattern(lhs) match { + case (pattern, N) => (pattern, S(rhs)) + case (pattern, S(lshRhs)) => (pattern, S(mkBinOp(lshRhs, and, rhs))) + } + case _ => (term, N) + } + + /** + * Generate a chain of `Let` from a list of bindings. + * + * @param bindings a list of bindings, + * @param body the final body + */ + def mkBindings(bindings: Ls[(Bool, Var, Term)], body: Term, defs: Set[Var]): Term = { + def rec(bindings: Ls[(Bool, Var, Term)], defs: Set[Var]): Term = + bindings match { + case Nil => body + case (head @ (isRec, nameVar, value)) :: tail => + if (defs.contains(head._2)) { + rec(tail, defs) + } else { + Let(isRec, nameVar, value, rec(tail, defs + head._2)) + } + } + rec(bindings, defs) + } + + /** + * Generate a chain of field selection to the given scrutinee. + * + * @param scrutinee the pattern matching scrutinee + * @param fields a list of pairs from field names to binding names + * @param body the final body + */ + def mkLetFromFields(scrutinee: Scrutinee, fields: Ls[Str -> Var], body: Term): Term = { + def rec(fields: Ls[Str -> Var]): Term = + fields match { + case Nil => body + case (field -> (aliasVar @ Var(alias))) :: tail => + Let(false, aliasVar, Sel(scrutinee.reference, Var(field)).desugaredFrom(scrutinee.term), rec(tail)) + } + rec(fields) + } +} diff --git a/shared/src/main/scala/mlscript/utils/package.scala b/shared/src/main/scala/mlscript/utils/package.scala index 9cc860c78b..9f861ad1c2 100644 --- a/shared/src/main/scala/mlscript/utils/package.scala +++ b/shared/src/main/scala/mlscript/utils/package.scala @@ -39,6 +39,8 @@ package object utils { def decapitalize: String = if (self.length === 0 || !self.charAt(0).isUpper) self else self.updated(0, self.charAt(0).toLower) + def pluralize(quantity: Int, es: Boolean = false): String = + if (quantity > 1) self + (if (es) "es" else "s") else self @SuppressWarnings(Array("org.wartremover.warts.Equals")) def ===(other: String): Bool = self.equals(other) } @@ -179,6 +181,7 @@ package object utils { def single[A: Ordering, B](ab: A -> B): SortedMap[A, B] = (SortedMap.newBuilder[A, B] += ab).result() } + def TODO(msg: String): Nothing = throw new NotImplementedError(msg) def die: Nothing = lastWords("Program reached and unexpected state.") def lastWords(msg: String): Nothing = throw new Exception(s"Internal Error: $msg") diff --git a/shared/src/test/diff/basics/Intersections.fun b/shared/src/test/diff/basics/Intersections.fun index ee600c5067..9f84b2e7aa 100644 --- a/shared/src/test/diff/basics/Intersections.fun +++ b/shared/src/test/diff/basics/Intersections.fun @@ -58,7 +58,7 @@ foo as Nothing :e let oops = (&) -//│ ╔══[ERROR] Illegal use of operator: & +//│ ╔══[ERROR] Illegal use of reserved operator: & //│ ║ l.60: let oops = (&) //│ ╙── ^^^ //│ ╔══[ERROR] identifier not found: & diff --git a/shared/src/test/diff/codegen/ReplHost.mls b/shared/src/test/diff/codegen/ReplHost.mls index b8414a5cb4..00687d025d 100644 --- a/shared/src/test/diff/codegen/ReplHost.mls +++ b/shared/src/test/diff/codegen/ReplHost.mls @@ -30,7 +30,8 @@ box0 = Box { inner = 0 } //│ │ undefined //│ └─┬ Query 1/1 //│ ├── Prelude: -//│ ├── Code: globalThis.box0 = new Box({ inner: 0 }); +//│ ├── Code: +//│ ├── globalThis.box0 = new Box({ inner: 0 }); //│ ├── Intermediate: Box { inner: 0 } //│ └── Reply: [success] Box { inner: 0 } //│ box0: Box[0] @@ -38,11 +39,12 @@ box0 = Box { inner = 0 } :ShowRepl box1 = Box { inner = 1 } -//│ ┌ Block at ReplHost.mls:40 +//│ ┌ Block at ReplHost.mls:41 //│ ├── No prelude //│ └─┬ Query 1/1 //│ ├── Prelude: -//│ ├── Code: globalThis.box1 = new Box({ inner: 1 }); +//│ ├── Code: +//│ ├── globalThis.box1 = new Box({ inner: 1 }); //│ ├── Intermediate: Box { inner: 1 } //│ └── Reply: [success] Box { inner: 1 } //│ box1: Box[1] @@ -50,11 +52,15 @@ box1 = Box { inner = 1 } :ShowRepl case box1 of { Box -> 0 } -//│ ┌ Block at ReplHost.mls:52 +//│ ┌ Block at ReplHost.mls:54 //│ ├── No prelude //│ └─┬ Query 1/1 -//│ ├── Prelude: let a; -//│ ├── Code: res = (a = box1, a instanceof Box ? 0 : (() => { throw new Error("non-exhaustive case expression");})()); +//│ ├── Prelude: +//│ ├── let a; +//│ ├── Code: +//│ ├── res = (a = box1, a instanceof Box ? 0 : (() => { +//│ ├── throw new Error("non-exhaustive case expression"); +//│ ├── })()); //│ ├── Intermediate: 0 //│ └── Reply: [success] 0 //│ res: 0 @@ -64,7 +70,7 @@ case box1 of { Box -> 0 } box1.Map (fun x -> add x 1) box1.Map (fun x -> add x 2) box1.Map (fun x -> Box { inner = x }) -//│ ┌ Block at ReplHost.mls:64 +//│ ┌ Block at ReplHost.mls:70 //│ ├─┬ Prelude //│ │ ├── Code //│ │ │ function add(x, y) { @@ -78,17 +84,20 @@ box1.Map (fun x -> Box { inner = x }) //│ │ undefined //│ ├─┬ Query 1/3 //│ │ ├── Prelude: -//│ │ ├── Code: res = box1.Map((x) => add(x)(1)); +//│ │ ├── Code: +//│ │ ├── res = box1.Map((x) => add(x)(1)); //│ │ ├── Intermediate: Box { inner: 2 } //│ │ └── Reply: [success] Box { inner: 2 } //│ ├─┬ Query 2/3 //│ │ ├── Prelude: -//│ │ ├── Code: res = box1.Map((x) => add(x)(2)); +//│ │ ├── Code: +//│ │ ├── res = box1.Map((x) => add(x)(2)); //│ │ ├── Intermediate: Box { inner: 3 } //│ │ └── Reply: [success] Box { inner: 3 } //│ └─┬ Query 3/3 //│ ├── Prelude: -//│ ├── Code: res = box1.Map((x) => new Box({ inner: x })); +//│ ├── Code: +//│ ├── res = box1.Map((x) => new Box({ inner: x })); //│ ├── Intermediate: Box { inner: Box { inner: 1 } } //│ └── Reply: [success] Box { inner: Box { inner: 1 } } //│ res: Box[int] diff --git a/shared/src/test/diff/mlscript/MatchBool.mls b/shared/src/test/diff/mlscript/MatchBool.mls new file mode 100644 index 0000000000..8f4b16a564 --- /dev/null +++ b/shared/src/test/diff/mlscript/MatchBool.mls @@ -0,0 +1,34 @@ + +def absImpl lt x = + case lt of + { true -> x + | false -> 0 - x } +//│ absImpl: bool -> int -> int +//│ = [Function: absImpl] + +// * TODO support this +:e +def abs x = + let r = x < 0 in absImpl r x +//│ ╔══[ERROR] Type mismatch in application: +//│ ║ l.12: let r = x < 0 in absImpl r x +//│ ║ ^^^^^^^^^ +//│ ╟── operator application of type `bool` does not match type `false & ?a | true & ?b` +//│ ║ l.12: let r = x < 0 in absImpl r x +//│ ║ ^^^^^ +//│ ╟── but it flows into reference with expected type `false & ?a | true & ?b` +//│ ║ l.12: let r = x < 0 in absImpl r x +//│ ║ ^ +//│ ╟── Note: constraint arises from reference: +//│ ║ l.3: case lt of +//│ ╙── ^^ +//│ abs: int -> (error | int) +//│ = [Function: abs] + + +def neg b = case b of + { true -> false + | false -> true } +//│ neg: bool -> bool +//│ = [Function: neg] + diff --git a/shared/src/test/diff/nu/LetRec.mls b/shared/src/test/diff/nu/LetRec.mls index af49fbd301..a1ca2c44a9 100644 --- a/shared/src/test/diff/nu/LetRec.mls +++ b/shared/src/test/diff/nu/LetRec.mls @@ -36,7 +36,7 @@ let rec f() = //│ // Query 1 //│ globalThis.f2 = function f2() { //│ return ((() => { -//│ f2(); +//│ return f2(); //│ })()); //│ }; //│ // End of generated code @@ -77,19 +77,7 @@ let rec f = 1 //│ f: 1 //│ = 1 -// TODO FIX undefined: should `return 1`; -:p -:js let rec f = 1 -//│ |#let| |#rec| |f| |#=|→|1|←| -//│ Parsed: let rec f = {1}; -//│ Desugared: rec def f: {1} -//│ AST: Def(true, f, Blk(...), false) -//│ // Query 1 -//│ globalThis.f5 = (() => { -//│ 1; -//│ })(); -//│ // End of generated code //│ f: 1 -//│ = undefined +//│ = 1 diff --git a/shared/src/test/diff/nu/LocalLets.mls b/shared/src/test/diff/nu/LocalLets.mls index 2c1be80749..cbccb80778 100644 --- a/shared/src/test/diff/nu/LocalLets.mls +++ b/shared/src/test/diff/nu/LocalLets.mls @@ -10,6 +10,6 @@ let f = //│ ╙── ^^^^^^^^^^ //│ f: 123 //│ Code generation encountered an error: -//│ unexpected definitions in blocks +//│ unsupported definitions in blocks diff --git a/shared/src/test/diff/ultimate/Misc.mls b/shared/src/test/diff/nu/Misc.mls similarity index 97% rename from shared/src/test/diff/ultimate/Misc.mls rename to shared/src/test/diff/nu/Misc.mls index 9a4fe3adce..798a1bfd7f 100644 --- a/shared/src/test/diff/ultimate/Misc.mls +++ b/shared/src/test/diff/nu/Misc.mls @@ -61,6 +61,8 @@ f([1, 2]) //│ res: error | int //│ = '1,2undefined' + + let f = ((x, y)) => x + y //│ f: (int, int,) -> int //│ = [Function: f3] @@ -68,15 +70,17 @@ let f = ((x, y)) => x + y :e f(1, 2) //│ ╔══[ERROR] Type mismatch in application: -//│ ║ l.69: f(1, 2) +//│ ║ l.71: f(1, 2) //│ ║ ^^^^^^^ //│ ╟── tuple literal of type `(1, 2,)` does not match type `((?a, ?b,),)` -//│ ║ l.69: f(1, 2) +//│ ║ l.71: f(1, 2) //│ ╙── ^^^^^^ //│ res: error | int //│ Runtime error: //│ TypeError: number 1 is not iterable (cannot read property Symbol(Symbol.iterator)) + + f((1, 2)) //│ res: int //│ = 3 diff --git a/shared/src/test/diff/nu/NamedArgs.mls b/shared/src/test/diff/nu/NamedArgs.mls new file mode 100644 index 0000000000..7f5b232491 --- /dev/null +++ b/shared/src/test/diff/nu/NamedArgs.mls @@ -0,0 +1,48 @@ +:NewParser + + +:w +class Foo(x: int) +//│ Defined class Foo +//│ ╔══[WARNING] Variable name 'int' already names a symbol in scope. If you want to refer to that symbol, you can use `scope.int`; if not, give your future readers a break and use another name :^) +//│ ║ l.5: class Foo(x: int) +//│ ╙── ^^^ +//│ Foo: (x: int & 'x,) -> (Foo with {x: 'x}) +//│ = [Function: Foo1] + +Foo(1) +//│ res: Foo & {x: 1} +//│ = Foo { x: 1 } + +Foo(x: 1) +//│ res: Foo & {x: 1} +//│ = Foo { x: 1 } + +:e +Foo(y: 1) +//│ ╔══[ERROR] Wrong tuple field name: found 'y' instead of 'x' +//│ ║ l.22: Foo(y: 1) +//│ ╙── ^^^^^^ +//│ res: error | Foo & {x: 1} +//│ = Foo { x: 1 } + + +// TODO: Here `x` is not currently treated as a field name +class Bar(x) +//│ Defined class Bar +//│ Bar: 'x -> (Bar with {x: 'x}) +//│ = [Function: Bar1] + +Bar(1) +//│ res: Bar & {x: 1} +//│ = Bar { x: 1 } + +Bar(x: 1) +//│ res: Bar & {x: 1} +//│ = Bar { x: 1 } + +// :e +Bar(y: 1) +//│ res: Bar & {x: 1} +//│ = Bar { x: 1 } + diff --git a/shared/src/test/diff/nu/New.mls b/shared/src/test/diff/nu/New.mls index 77bd73d4fb..37a5d98563 100644 --- a/shared/src/test/diff/nu/New.mls +++ b/shared/src/test/diff/nu/New.mls @@ -10,15 +10,17 @@ let f = Foo(1) //│ f: Foo & {x: 1} //│ = Foo { x: 1 } -:ge let f = new Foo(1) //│ f: Foo & {x: 1} -//│ Code generation encountered an error: -//│ cannot generate code for term New(Some((TypeName(Foo),1,)), TypingUnit(List())) +//│ = Foo { x: 1 } + +if f is Foo then 1 else 0 +//│ res: 0 | 1 +//│ = 1 -// TODO if f is Foo(a) then a else 0 -//│ /!!!\ Uncaught error: scala.NotImplementedError: an implementation is missing +//│ res: 0 | 1 +//│ = 1 // case f of // { Foo -> @@ -30,4 +32,19 @@ if f is Foo(a) then a else 0 // Foo(A) =:= Foo & { x: A } +fun test(x) = if x is Foo(a) then a +//│ test: (Foo with {x: 'x}) -> 'x +//│ = [Function: test] + +test(f) +//│ res: 1 +//│ = 1 + +class Point(x, y) +//│ Defined class Point +//│ Point: ('x, 'y,) -> (Point with {x: 'x, y: 'y}) +//│ = [Function: Point1] +let origin = new Point(0, 0) +//│ origin: Point & {x: 0, y: 0} +//│ = Point { x: 0, y: 0 } diff --git a/shared/src/test/diff/nu/ParserFailures.mls b/shared/src/test/diff/nu/ParserFailures.mls new file mode 100644 index 0000000000..61e2ff5a33 --- /dev/null +++ b/shared/src/test/diff/nu/ParserFailures.mls @@ -0,0 +1,17 @@ +:NewParser +:NoJS + +// FIXME: Interleaved let bindings are not implemented in `IfOpsApp`. +if x + == 0 then "bad" + let y = f(z) + == y * y then 0 +//│ /!!!\ Uncaught error: scala.NotImplementedError: an implementation is missing + +// FIXME: Interleaved let bindings are not implemented in `IfOpsApp`. +fun tt(x) = + if x + is A() then "A" + let y = 0 + is B() then "B" +//│ /!!!\ Uncaught error: scala.NotImplementedError: an implementation is missing diff --git a/shared/src/test/diff/parser/IfThenElse.mls b/shared/src/test/diff/parser/IfThenElse.mls index abc2ccc7e4..1fc442e453 100644 --- a/shared/src/test/diff/parser/IfThenElse.mls +++ b/shared/src/test/diff/parser/IfThenElse.mls @@ -441,7 +441,7 @@ if let Some(x) = v and cond then 123 // MLscript: if v is Some(x) and x is Left(y) then 123 //│ |#if| |v| |is| |Some|(|x|)| |and| |x| |is| |Left|(|y|)| |#then| |123| -//│ Parsed: {if v is Some (x,) and x is (Left (y,)) then 123} +//│ Parsed: {if (and (is (v,) (Some (x,),),) (is (x,) (Left (y,),),)) then 123} // ML: let Some(x) = v diff --git a/shared/src/test/diff/parser/Misc.mls b/shared/src/test/diff/parser/Misc.mls index e501d9bcb9..1ebc7d43dc 100644 --- a/shared/src/test/diff/parser/Misc.mls +++ b/shared/src/test/diff/parser/Misc.mls @@ -131,19 +131,9 @@ if f of x is Some then 1 else 0 //│ |#if| |f| |#of| |x| |is| |Some| |#then| |1| |#else| |0| //│ Parsed: {if (f (is (x,) (Some,),)) then 1 else 0} -// :dp if f of 0 and g of 1 then "ok" //│ |#if| |f| |#of| |0| |and| |g| |#of| |1| |#then| |"ok"| -//│ ╔══[PARSE ERROR] Unexpected 'then'/'else' clause -//│ ║ l.135: if f of 0 and g of 1 then "ok" -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ -//│ ╔══[PARSE ERROR] Expected 'then'/'else' clause; found application instead -//│ ║ l.135: if f of 0 and g of 1 then "ok" -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Note: 'if' expression started here: -//│ ║ l.135: if f of 0 and g of 1 then "ok" -//│ ╙── ^^ -//│ Parsed: {if (f (undefined,)) then undefined} +//│ Parsed: {if (f (and (0,) (g (1,),),)) then "ok"} A and B or C and D //│ |A| |and| |B| |or| |C| |and| |D| diff --git a/shared/src/test/diff/parser/WeirdIfs.mls b/shared/src/test/diff/parser/WeirdIfs.mls new file mode 100644 index 0000000000..b61b6cc3a3 --- /dev/null +++ b/shared/src/test/diff/parser/WeirdIfs.mls @@ -0,0 +1,58 @@ + +if else e +//│ |#if| |#else| |e| +//│ Parsed: {if else e} + +if x is else e +//│ |#if| |x| |is| |#else| |e| +//│ Parsed: {if x is else e} + +:pe +if x is + else e +//│ |#if| |x| |is|→|#else| |e|←| +//│ ╔══[PARSE ERROR] Unexpected indented block in expression position +//│ ║ l.12: else e +//│ ╙── ^^ +//│ ╔══[PARSE ERROR] Unexpected end of input; an expression was expected here +//│ ║ l.12: else e +//│ ╙── ^ +//│ ╔══[PARSE ERROR] Expected 'then'/'else' clause; found operator application instead +//│ ║ l.11: if x is +//│ ║ ^^^^ +//│ ║ l.12: else e +//│ ║ ^^ +//│ ╟── Note: 'if' expression started here: +//│ ║ l.11: if x is +//│ ╙── ^^ +//│ Parsed: {if (is (x,) (undefined,)) then undefined} + +:pe +if x is + P else e +//│ |#if| |x| |is|→|P| |#else| |e|←| +//│ ╔══[PARSE ERROR] Unexpected 'else' keyword here +//│ ║ l.32: P else e +//│ ╙── ^^^^ +//│ ╔══[PARSE ERROR] Expected 'then'/'else' clause; found operator application instead +//│ ║ l.31: if x is +//│ ║ ^^^^ +//│ ║ l.32: P else e +//│ ║ ^^^^ +//│ ╟── Note: 'if' expression started here: +//│ ║ l.31: if x is +//│ ╙── ^^ +//│ Parsed: {if (is (x,) ({P},)) then undefined} + + +// TODO(Luyu): move to another test file +if x is + Some(xv) and y is Left(yv) then xv + yv + Some(xv) and y is Right(yv2) then xv + yv2 + else els +//│ |#if| |x| |is|→|Some|(|xv|)| |and| |y| |is| |Left|(|yv|)| |#then| |xv| |+| |yv|↵|Some|(|xv|)| |and| |y| |is| |Right|(|yv2|)| |#then| |xv| |+| |yv2|↵|#else| |els|←| +//│ Parsed: {if x is ‹(and (Some (xv,),) (is (y,) (Left (yv,),),)) then + (xv,) (yv,); (and (Some (xv,),) (is (y,) (Right (yv2,),),)) then + (xv,) (yv2,); else els›} + + + + diff --git a/shared/src/test/diff/typegen/TypegenTerms.mls b/shared/src/test/diff/typegen/TypegenTerms.mls index 457a97a3f3..afd4d230ad 100644 --- a/shared/src/test/diff/typegen/TypegenTerms.mls +++ b/shared/src/test/diff/typegen/TypegenTerms.mls @@ -66,9 +66,6 @@ rec def l (a: int) = l rec def m (a: int) (b: int) = m def f: ('c -> 'a as 'a) -> 'c -> int // recursion type functions -//│ l: 'l -//│ where -//│ 'l :> int -> 'l //│ /!!!\ Uncaught error: mlscript.codegen.CodeGenError: Cannot generate type for `where` clause List((α111,Bounds(Function(Tuple(List((None,Field(None,TypeName(int))))),α111),Top))) :ts @@ -76,19 +73,19 @@ def f: ('c -> 'a as 'a) -> 'c -> int 1: ? { a = "hello" }: { a: string } & { b: int } //│ ╔══[ERROR] Type mismatch in type ascription: -//│ ║ l.76: 1: ? +//│ ║ l.73: 1: ? //│ ║ ^ //│ ╟── integer literal of type `1` does not match type `nothing` //│ ╟── Note: constraint arises from type wildcard: -//│ ║ l.76: 1: ? +//│ ║ l.73: 1: ? //│ ╙── ^ //│ res: anything //│ ╔══[ERROR] Type mismatch in type ascription: -//│ ║ l.77: { a = "hello" }: { a: string } & { b: int } +//│ ║ l.74: { a = "hello" }: { a: string } & { b: int } //│ ║ ^^^^^^^^^^^^^^^ //│ ╟── record literal of type `{a: "hello"}` does not have field 'b' //│ ╟── Note: constraint arises from intersection type: -//│ ║ l.77: { a = "hello" }: { a: string } & { b: int } +//│ ║ l.74: { a = "hello" }: { a: string } & { b: int } //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ res: {a: string, b: int} //│ // start ts @@ -155,9 +152,6 @@ def weird: ((int, int) -> 'a) as 'a def weird: ('a -> (int, int)) as 'a def weird: ((int, 'a) as 'a) -> int def weird: ((int, bool) | 'a) -> 'a -//│ weird: 'a -//│ where -//│ 'a :> (int, int,) -> 'a //│ /!!!\ Uncaught error: mlscript.codegen.CodeGenError: Cannot generate type for `where` clause List((α233,Bounds(Function(Tuple(List((None,Field(None,TypeName(int))), (None,Field(None,TypeName(int))))),α233),Top))) diff --git a/shared/src/test/diff/ucs/DirectLines.mls b/shared/src/test/diff/ucs/DirectLines.mls new file mode 100644 index 0000000000..255e0bbc17 --- /dev/null +++ b/shared/src/test/diff/ucs/DirectLines.mls @@ -0,0 +1,73 @@ +:NewParser + +fun f(x, y) = + if + x == 0 then "x" + y == 0 then "y" + _ then "nah" +//│ f: (number, number,) -> ("nah" | "x" | "y") +//│ = [Function: f] + +class Option +class Some(value): Option +class None: Option +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Option: () -> Option +//│ = [Function: Option1] +//│ Some: 'value -> (Some with {value: 'value}) +//│ = [Function: Some1] +//│ None: () -> None +//│ = [Function: None1] + +fun isValid(x) = if x then false else true +//│ isValid: anything -> bool +//│ = [Function: isValid] + +fun f(x, allowNone) = + if x + is Some(x) and isValid(x) then "good" + is None() and allowNone then "okay" + is _ then "bad" +//│ f: (anything, anything,) -> ("bad" | "good" | "okay") +//│ = [Function: f1] + +:w +fun f(x, y, z) = + if + x == 0 then "x" + y == + 1 then "y = 1" + 2 and z == + 0 then "z = 0" + 9 then "z = 9" + _ then "bruh" + 3 then "y = 3" + _ then "bruh" +//│ ╔══[WARNING] Found a duplicated else branch +//│ ║ l.47: _ then "bruh" +//│ ║ ^^^^^^ +//│ ╟── The first else branch was declared here. +//│ ║ l.45: _ then "bruh" +//│ ╙── ^^^^^^ +//│ f: (number, number, number,) -> ("bruh" | "x" | "y = 1" | "y = 3" | "z = 0" | "z = 9") +//│ = [Function: f2] + +:w +fun f(a, b) = + if + a == 0 then 0 + b == + 1 then 1 + 2 then 2 + _ then 7 + else 3 +//│ ╔══[WARNING] Found a duplicated else branch +//│ ║ l.65: else 3 +//│ ║ ^ +//│ ╟── The first else branch was declared here. +//│ ║ l.64: _ then 7 +//│ ╙── ^ +//│ f: (number, number,) -> (0 | 1 | 2 | 7) +//│ = [Function: f3] diff --git a/shared/src/test/diff/ucs/ErrorMessage.mls b/shared/src/test/diff/ucs/ErrorMessage.mls new file mode 100644 index 0000000000..a483fa0902 --- /dev/null +++ b/shared/src/test/diff/ucs/ErrorMessage.mls @@ -0,0 +1,30 @@ +:NewParser + +class Point(x, y) +//│ Defined class Point +//│ Point: ('x, 'y,) -> (Point with {x: 'x, y: 'y}) +//│ = [Function: Point1] + +:e +:ge +fun f(p) = + if p is + Point(x, y, z) then x + y + z +//│ ╔══[ERROR] class Point expects 2 parameters but found 3 parameters +//│ ║ l.12: Point(x, y, z) then x + y + z +//│ ╙── ^^^^^^^^^^^^^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun g(xs) = + if xs is + head :: _ then head +//│ ╔══[ERROR] Cannot find operator `::` in the context +//│ ║ l.24: head :: _ then head +//│ ╙── ^^ +//│ g: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared diff --git a/shared/src/test/diff/ucs/Exhaustiveness.mls b/shared/src/test/diff/ucs/Exhaustiveness.mls new file mode 100644 index 0000000000..c56f142300 --- /dev/null +++ b/shared/src/test/diff/ucs/Exhaustiveness.mls @@ -0,0 +1,33 @@ +:NewParser +:NoJS + +class A() +class B() +class C() +//│ Defined class A +//│ Defined class B +//│ Defined class C +//│ A: () -> A +//│ B: () -> B +//│ C: () -> C + +:e +fun f(x, y) = + if + y is A and + x is + A then 0 + B then 1 + C then 2 + y is B and + x is + A then 4 +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.23: x is +//│ ║ ^^^^ +//│ ╟── The scrutinee at this position misses 2 cases. +//│ ║ l.23: x is +//│ ║ ^ +//│ ╟── [Missing Case 1/2] `B` +//│ ╙── [Missing Case 2/2] `C` +//│ f: (anything, anything,) -> error diff --git a/shared/src/test/diff/ucs/Humiliation.mls b/shared/src/test/diff/ucs/Humiliation.mls new file mode 100644 index 0000000000..400edc3d3b --- /dev/null +++ b/shared/src/test/diff/ucs/Humiliation.mls @@ -0,0 +1,193 @@ +:NewParser + + +class Foo(x) +//│ Defined class Foo +//│ Foo: 'x -> (Foo with {x: 'x}) +//│ = [Function: Foo1] + +if 1 is 1 then 1 else 0 +//│ res: 0 | 1 +//│ = 1 + +fun test(x) = if x is 1 then 0 else 1 +//│ test: number -> (0 | 1) +//│ = [Function: test] + +// It should report duplicated branches. +:w +fun testF(x) = if x is + Foo(a) then a + Foo(a) then a +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ testF: (Foo with {x: 'x}) -> 'x +//│ = [Function: testF] + +class Bar(y, z) +//│ Defined class Bar +//│ Bar: ('y, 'z,) -> (Bar with {y: 'y, z: 'z}) +//│ = [Function: Bar1] + +fun test(f) = if f is + Foo(a) then a + Bar(b, c) then b + c +//│ test: (Bar & {y: int, z: int} | (Foo with {x: 'x})) -> (int | 'x) +//│ = [Function: test1] + + +class Pair(fst, snd) +//│ Defined class Pair +//│ Pair: ('fst, 'snd,) -> (Pair with {fst: 'fst, snd: 'snd}) +//│ = [Function: Pair1] + +fun f(x) = + if x is + Pair(0, 0) then "zeros" + Pair(1, 1) then "ones" + Pair(y, 1) then x + _ then "nah" +//│ f: (Pair & {fst: number, snd: number} & 'a | ~Pair) -> ("nah" | "ones" | "zeros" | 'a) +//│ = [Function: f] + +class Z() +class O() +//│ Defined class Z +//│ Defined class O +//│ Z: () -> Z +//│ = [Function: Z1] +//│ O: () -> O +//│ = [Function: O1] + +// This is not exhaustive. +:e +:ge +fun foo(x) = if x is + Pair(Z(), Z()) then "zeros" + Pair(O(), O()) then "ones" +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.65: fun foo(x) = if x is +//│ ║ ^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.66: Pair(Z(), Z()) then "zeros" +//│ ║ ^^^ +//│ ╟── [Missing Case 1/1] `O` +//│ ╟── It first appears here. +//│ ║ l.67: Pair(O(), O()) then "ones" +//│ ╙── ^^^ +//│ foo: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +// Change `Pair` to a real pair. +:e +:ge +fun foo(x) = if x is + (Z(), Z()) then "zeros" + (O(), O()) then "ones" +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.85: fun foo(x) = if x is +//│ ║ ^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.86: (Z(), Z()) then "zeros" +//│ ║ ^^^ +//│ ╟── [Missing Case 1/1] `O` +//│ ╟── It first appears here. +//│ ║ l.87: (O(), O()) then "ones" +//│ ╙── ^^^ +//│ foo: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun foo(x) = if x is + Pair(a, b) then if a is + Z() then if b is + Z() then "zeros" + O() then if b is + O() then "ones" +//│ foo: (Pair & {fst: O | Z, snd: nothing}) -> ("ones" | "zeros") +//│ = [Function: foo2] + +fun foo(x) = if x is + Pair(a, b) then if a is + Z() then if b is + Z() then "zeros" + else "???" + O() then if b is + O() then "ones" +//│ foo: (Pair & {fst: O | Z, snd: O}) -> ("???" | "ones" | "zeros") +//│ = [Function: foo3] + +fun foo(x) = if x is + Pair(a, b) then if a is + Z() then if b is + Z() then "zeros" + else "???" + O() then if b is + O() then "zeros" + else "???" +//│ foo: (Pair & {fst: O | Z}) -> ("???" | "zeros") +//│ = [Function: foo4] + +class S(pred) +//│ Defined class S +//│ S: 'pred -> (S with {pred: 'pred}) +//│ = [Function: S1] + +// TODO: Cannot check exhaustiveness of nested UCS yet. +fun foo(x) = if x is + Pair(a, b) then if a is + Z() then if b is + S(x) then x + else "???" + O() then if b is + O() then "zeros" + else "???" +//│ foo: (Pair & {fst: O | Z, snd: (S with {pred: 'pred}) | ~S}) -> ("???" | "zeros" | 'pred) +//│ = [Function: foo5] + +:re +foo(Pair(Z(), Z())) +//│ res: "???" | "zeros" +//│ Runtime error: +//│ Error: non-exhaustive case expression + +:e +:ge +fun foo(x) = if x is + Pair(Z(), Z()) then "zeros" + Pair(O(), O()) then "ones" + Pair(y, O()) then x +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.157: fun foo(x) = if x is +//│ ║ ^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.158: Pair(Z(), Z()) then "zeros" +//│ ║ ^^^ +//│ ╟── [Missing Case 1/1] `Z` +//│ ╟── It first appears here. +//│ ║ l.158: Pair(Z(), Z()) then "zeros" +//│ ╙── ^^^ +//│ foo: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun foo(x, y) = if x is Z() and y is O() then 0 else 1 +//│ foo: (anything, anything,) -> (0 | 1) +//│ = [Function: foo7] + +:pe +fun foo(x, y) = if x is + Z() and y is O() then 0 else 1 +//│ ╔══[PARSE ERROR] Unexpected 'else' keyword here +//│ ║ l.181: Z() and y is O() then 0 else 1 +//│ ╙── ^^^^ +//│ foo: (Z, O,) -> 0 +//│ = [Function: foo8] + +fun foo(x, y) = + if x is + Z() and y is O() then 0 + else 1 +//│ foo: (anything, anything,) -> (0 | 1) +//│ = [Function: foo9] diff --git a/shared/src/test/diff/ucs/InterleavedLet.mls b/shared/src/test/diff/ucs/InterleavedLet.mls new file mode 100644 index 0000000000..d9f3144dde --- /dev/null +++ b/shared/src/test/diff/ucs/InterleavedLet.mls @@ -0,0 +1,276 @@ +:NewParser + +fun f(x) = + if x == + let v = 0 + v then v + else 0 +//│ f: number -> 0 +//│ = [Function: f] + +class Option +class Some(value): Option +class None: Option +class Either +class Left(leftValue): Either +class Right(rightValue): Either +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Defined class Either +//│ Defined class Left +//│ Defined class Right +//│ Option: () -> Option +//│ = [Function: Option1] +//│ Some: 'value -> (Some with {value: 'value}) +//│ = [Function: Some1] +//│ None: () -> None +//│ = [Function: None1] +//│ Either: () -> Either +//│ = [Function: Either1] +//│ Left: 'leftValue -> (Left with {leftValue: 'leftValue}) +//│ = [Function: Left1] +//│ Right: 'rightValue -> (Right with {rightValue: 'rightValue}) +//│ = [Function: Right1] + +fun q(x) = + if + x is Some and x is Some and x is Some then 0 +//│ q: Some -> 0 +//│ = [Function: q] + +// FIXME +:w +fun p(x, y) = + if + x is Some and y is None then 0 + y is Some and x is Some then 1 + x is Some and y is Some then 0 +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ p: (Some, None | Some,) -> (0 | 1) +//│ = [Function: p] + +fun h(x, y) = + if x is + None then y + let y_square = y * y + Some(z) then z + y_square +//│ h: (None | Some & {value: int}, int,) -> int +//│ = [Function: h] + +h(Some(5), 6) +//│ res: int +//│ = 41 + +fun h(x, y) = + if x is + None then y + let y_square = y * y + Some(y_square) then 0 +//│ h: (None | Some, int & 'a,) -> (0 | 'a) +//│ = [Function: h1] + +fun f(a, y) = + if a is + Some(v) and v is + Left(x) then x + let y = v + 1 + Right(x) then x + y + else 0 +//│ f: (Some & {value: int} | ~Some, anything,) -> int +//│ = [Function: f1] + +:pe +fun q(a) = + if a is + Left(x) then x + let y = a + 1 + then y +//│ ╔══[PARSE ERROR] Expected an expression; found a 'then'/'else' clause instead +//│ ║ l.88: let y = a + 1 +//│ ║ ^^^^^ +//│ ║ l.89: then y +//│ ╙── ^^^^^^^^^^ +//│ q: (Left with {leftValue: 'leftValue}) -> 'leftValue +//│ = [Function: q1] + +class A() +class B() +//│ Defined class A +//│ Defined class B +//│ A: () -> A +//│ = [Function: A1] +//│ B: () -> B +//│ = [Function: B1] + +fun w() = + if + A then "A" + let y = 0 + B then "B" + else "?" +//│ w: () -> ("?" | "A" | "B") +//│ = [Function: w] + +w() +//│ res: "?" | "A" | "B" +//│ = '?' + +fun i(x) = + if x is + A() then "A" + let y = 0 + B() then "B" +//│ i: (A | B) -> ("A" | "B") +//│ = [Function: i] + +fun inc(x) = x + 1 +//│ inc: int -> int +//│ = [Function: inc] + +fun qq(x, z) = + if x == + let y = inc(z) + y * y then 0 + else 0 +//│ qq: (number, int,) -> 0 +//│ = [Function: qq] + +fun bruh(x) = + if + x == 0 then 0 + let y = 1 + else y +//│ bruh: number -> (0 | 1) +//│ = [Function: bruh] + +fun f1(x) = x + 1 +fun f2(x, y) = x + y +//│ f1: int -> int +//│ = [Function: f11] +//│ f2: (int, int,) -> int +//│ = [Function: f2] + +fun ff(x) = + if + x == 0 then 0 + let y = f1(x) + let z = f2(x, y) + z == 1 then 1 + z == 2 then 2 + else 0 +//│ ff: int -> (0 | 1 | 2) +//│ = [Function: ff] + +fun ip(y) = + if q(y) and + let z = inc(y) + y == z * z then "bruh" + else "rocks" +//│ ip: nothing -> ("bruh" | "rocks") +//│ = [Function: ip] + +fun tr(x) = + if x is + Some(v) then v + let tmp = 1 + None then tmp +//│ tr: (None | (Some with {value: 'value})) -> (1 | 'value) +//│ = [Function: tr] + +class Pair(fst, snd) +class List +class Nil: List +class Cons(head, tail): List +//│ Defined class Pair +//│ Defined class List +//│ Defined class Nil +//│ Defined class Cons +//│ Pair: ('fst, 'snd,) -> (Pair with {fst: 'fst, snd: 'snd}) +//│ = [Function: Pair1] +//│ List: () -> List +//│ = [Function: List1] +//│ Nil: () -> Nil +//│ = [Function: Nil1] +//│ Cons: ('head, 'tail,) -> (Cons with {head: 'head, tail: 'tail}) +//│ = [Function: Cons1] + +fun cat2(s, t) = concat(s)(t) +fun cat3(a, b, c) = cat2(cat2(a, b), c) +//│ cat2: (string, string,) -> string +//│ = [Function: cat2] +//│ cat3: (string, string, string,) -> string +//│ = [Function: cat3] + +:js +fun showList(xs) = + if xs is + Nil then "" + Cons(head, Nil()) then toString(head) + Cons(head, tail) then cat3(toString(head), ", ", showList(tail)) +//│ // Prelude +//│ function toString(x) { +//│ return String(x); +//│ } +//│ // Query 1 +//│ globalThis.showList = function showList(xs) { +//│ return ((() => { +//│ let a; +//│ return (a = xs, a instanceof Nil ? "" : a instanceof Cons ? ((head) => ((tmp0) => ((tail) => tmp0 instanceof Nil ? toString(head) : cat3(toString(head), ", ", showList(tail)))(xs.tail))(xs.tail))(xs.head) : (() => { +//│ throw new Error("non-exhaustive case expression"); +//│ })()); +//│ })()); +//│ }; +//│ // End of generated code +//│ showList: 'a -> string +//│ where +//│ 'a <: Cons & {tail: 'a} | Nil +//│ = [Function: showList] + +let zeroToThree = Cons(0, Cons(1, Cons(2, Cons(3, Nil())))) +//│ zeroToThree: Cons & {head: 0, tail: Cons & {head: 1, tail: Cons & {head: 2, tail: Cons & {head: 3, tail: Nil}}}} +//│ = Cons { +//│ head: 0, +//│ tail: Cons { head: 1, tail: Cons { head: 2, tail: [Cons] } } +//│ } + +showList(zeroToThree) +//│ res: string +//│ = '0, 1, 2, 3' + +// FIXME: This needs lifting functions. +fun mapPartition(f, xs) = + if xs is + Nil then Pair(Nil(), Nil()) + Cons(x, xs) and f(x) is + let res = mapPartition(f, xs) + let l = res.fst + let r = res.snd + Left(v) then Pair(Cons(v, l), r) + Right(v) then Pair(l, Cons(v, r)) +//│ mapPartition: ('head -> ((Left with {leftValue: 'head0}) | (Right with {rightValue: 'head1})), 'a,) -> (Pair with {fst: 'fst, snd: 'tail}) +//│ where +//│ 'tail :> (Cons with {head: 'head1, tail: 'tail}) | Nil +//│ 'fst :> Nil | (Cons with {head: 'head0, tail: 'fst}) +//│ 'a <: (Cons with {head: 'head, tail: 'a}) | Nil +//│ = [Function: mapPartition] + +// FIXME: Something wrong with code generation. +mapPartition(x => if x % 2 == 0 then Left(x) else Right(x), zeroToThree) +//│ res: Pair with {fst: 'fst, snd: 'tail} +//│ where +//│ 'tail :> (Cons with {head: 0 | 1 | 2 | 3, tail: 'tail}) | Nil +//│ 'fst :> Nil | (Cons with {head: 0 | 1 | 2 | 3, tail: 'fst}) +//│ Runtime error: +//│ RangeError: Maximum call stack size exceeded + +fun mn(a) = + if a is + Some(x) and x is + Left(a) then "left-defined" + let y = x + 1 + Right(b) then "right-defined" + None then "undefined" +//│ mn: (None | Some & {value: nothing}) -> ("left-defined" | "right-defined" | "undefined") +//│ = [Function: mn] diff --git a/shared/src/test/diff/ucs/NestedBranches.mls b/shared/src/test/diff/ucs/NestedBranches.mls new file mode 100644 index 0000000000..e428fda032 --- /dev/null +++ b/shared/src/test/diff/ucs/NestedBranches.mls @@ -0,0 +1,143 @@ +:NewParser + + +class Option +class Some(value): Option +class None: Option +class Either +class Left(leftValue): Either +class Right(rightValue): Either +class List +class Nil: List +class Cons(head, tail): List +class Pair(fst, snd) +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Defined class Either +//│ Defined class Left +//│ Defined class Right +//│ Defined class List +//│ Defined class Nil +//│ Defined class Cons +//│ Defined class Pair +//│ Option: () -> Option +//│ = [Function: Option1] +//│ Some: 'value -> (Some with {value: 'value}) +//│ = [Function: Some1] +//│ None: () -> None +//│ = [Function: None1] +//│ Either: () -> Either +//│ = [Function: Either1] +//│ Left: 'leftValue -> (Left with {leftValue: 'leftValue}) +//│ = [Function: Left1] +//│ Right: 'rightValue -> (Right with {rightValue: 'rightValue}) +//│ = [Function: Right1] +//│ List: () -> List +//│ = [Function: List1] +//│ Nil: () -> Nil +//│ = [Function: Nil1] +//│ Cons: ('head, 'tail,) -> (Cons with {head: 'head, tail: 'tail}) +//│ = [Function: Cons1] +//│ Pair: ('fst, 'snd,) -> (Pair with {fst: 'fst, snd: 'snd}) +//│ = [Function: Pair1] + + + +fun optionApply(x, y, f) = + if x is + Some(xv) and y is + Some(yv) then Some(f(xv, yv)) + None() then None() + None() then None() +//│ optionApply: (None | (Some with {value: 'value}), None | (Some with {value: 'value0}), ('value, 'value0,) -> 'value1,) -> (None | (Some with {value: 'value1})) +//│ = [Function: optionApply] + + + +fun mapPartition(f, xs) = if xs is + Nil then Pair(Nil, Nil) + Cons(x, xs) and mapPartition(f, xs) is Pair(l, r) and f(x) is + Left(v) then Pair(Cons(v, l), r) + Right(v) then Pair(l, Cons(v, r)) +//│ mapPartition: ('head -> ((Left with {leftValue: 'head0}) | (Right with {rightValue: 'head1})), 'a,) -> (Pair with {fst: 'fst, snd: 'tail}) +//│ where +//│ 'tail :> (Cons with {head: 'head1, tail: 'tail}) | () -> Nil +//│ 'fst :> () -> Nil | (Cons with {head: 'head0, tail: 'fst}) +//│ 'a <: (Cons with {head: 'head, tail: 'a}) | Nil +//│ = [Function: mapPartition] + +fun mapPartition(f, xs) = if xs is + Nil then Pair(Nil, Nil) + Cons(x, xs) and + mapPartition(f, xs) is Pair(l, r) and f(x) is + Left(v) then Pair(Cons(v, l), r) + Right(v) then Pair(l, Cons(v, r)) +//│ mapPartition: ('head -> ((Left with {leftValue: 'head0}) | (Right with {rightValue: 'head1})), 'a,) -> (Pair with {fst: 'fst, snd: 'tail}) +//│ where +//│ 'tail :> (Cons with {head: 'head1, tail: 'tail}) | () -> Nil +//│ 'fst :> () -> Nil | (Cons with {head: 'head0, tail: 'fst}) +//│ 'a <: (Cons with {head: 'head, tail: 'a}) | Nil +//│ = [Function: mapPartition1] + +fun mapPartition(f, xs) = if xs is + Nil then + Pair(Nil, Nil) + Cons(x, xs) and + mapPartition(f, xs) is + Pair(l, r) and + f(x) is + Left(v) then + Pair(Cons(v, l), r) + Right(v) then + Pair(l, Cons(v, r)) +//│ mapPartition: ('head -> ((Left with {leftValue: 'head0}) | (Right with {rightValue: 'head1})), 'a,) -> (Pair with {fst: 'fst, snd: 'tail}) +//│ where +//│ 'tail :> (Cons with {head: 'head1, tail: 'tail}) | () -> Nil +//│ 'fst :> () -> Nil | (Cons with {head: 'head0, tail: 'fst}) +//│ 'a <: (Cons with {head: 'head, tail: 'a}) | Nil +//│ = [Function: mapPartition2] + + +// TODO make this one work (needs tuple support) +fun mapPartition(f, xs) = if xs is + Nil then (Nil, Nil) + Cons(x, xs) and mapPartition(f, xs) is (l, r) and f(x) is + Left(v) then (Cons(v, l), r) + Right(v) then (l, Cons(v, r)) +//│ ╔══[ERROR] The case when this is false is not handled: is (mapPartition (f, xs,),) (l, r,) +//│ ║ l.105: Cons(x, xs) and mapPartition(f, xs) is (l, r) and f(x) is +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ mapPartition: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + + + +// * Vertical alignment is not allowed! (good) +:pe +:w +:e +:ge +fun mapPartition(f, xs) = if xs is + Nil then (Nil, Nil) + Cons(x, xs) and mapPartition(f, xs) is (l, r) + and f(x) is Left(v) then (Cons(v, l), r) + Right(v) then (l, Cons(v, r)) +//│ ╔══[PARSE ERROR] Unexpected 'then' keyword here +//│ ║ l.126: Right(v) then (l, Cons(v, r)) +//│ ╙── ^^^^ +//│ ╔══[WARNING] Paren-less applications should use the 'of' keyword +//│ ║ l.125: and f(x) is Left(v) then (Cons(v, l), r) +//│ ║ ^^^^^^^^^^^^^^^ +//│ ║ l.126: Right(v) then (l, Cons(v, r)) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[ERROR] type identifier not found: Tuple#2 +//│ ╙── +//│ mapPartition: (anything, 'a,) -> ((() -> Nil, () -> Nil,) | error) +//│ where +//│ 'a <: Cons & {tail: 'a} | Nil +//│ Code generation encountered an error: +//│ unknown match case: Tuple#2 + + diff --git a/shared/src/test/diff/ucs/NestedPattern.mls b/shared/src/test/diff/ucs/NestedPattern.mls new file mode 100644 index 0000000000..f135df5590 --- /dev/null +++ b/shared/src/test/diff/ucs/NestedPattern.mls @@ -0,0 +1,70 @@ +:NewParser +:NoJS + +class Option +class Some(value): Option +class None: Option +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Option: () -> Option +//│ Some: 'value -> (Some with {value: 'value}) +//│ None: () -> None + +class Either +class Left(leftValue): Either +class Right(rightValue): Either +//│ Defined class Either +//│ Defined class Left +//│ Defined class Right +//│ Either: () -> Either +//│ Left: 'leftValue -> (Left with {leftValue: 'leftValue}) +//│ Right: 'rightValue -> (Right with {rightValue: 'rightValue}) + +fun compute(v) = + if v is + Left(Some(x)) then x * 5 + Left(None()) then 1 + Right(Some(x)) then x * 3 + Right(None()) then 0 +//│ compute: (Left & {leftValue: None | Some & {value: int}} | Right & {rightValue: None | Some & {value: int}}) -> int + +fun crazy(v) = + if v is + Some(Some(Some(Some(Some(Some(None())))))) then "bruh!" + _ then "lol" +//│ crazy: anything -> ("bruh!" | "lol") + + +// * TODO(@chengluyu) fix these missing locations + +:e +fun f(x) = + if x is + (0, 0) then "zeros" + (1, 1) then "ones" + _ then "bruh" +//│ ╔══[ERROR] type identifier not found: Tuple#2 +//│ ╙── +//│ f: error -> error + +:e +fun f(x) = + if x is + (0, 0) then "zeros" + (1, 1) then "ones" + (y, 1) then x + _ then "que?" +//│ ╔══[ERROR] type identifier not found: Tuple#2 +//│ ╙── +//│ f: error -> error + +:e +fun f(p) = + if p is + Some((x, y)) then x + y + None() then 0 +//│ ╔══[ERROR] type identifier not found: Tuple#2 +//│ ╙── +//│ f: (None | Some & {value: error}) -> (0 | error) + diff --git a/shared/src/test/diff/ucs/PlainConditionals.mls b/shared/src/test/diff/ucs/PlainConditionals.mls new file mode 100644 index 0000000000..d45751920e --- /dev/null +++ b/shared/src/test/diff/ucs/PlainConditionals.mls @@ -0,0 +1,43 @@ +:NewParser + + +class Pair(fst, snd) +//│ Defined class Pair +//│ Pair: ('fst, 'snd,) -> (Pair with {fst: 'fst, snd: 'snd}) +//│ = [Function: Pair1] + + + +Pair(0, 1) is Pair(a, b) +//│ res: bool +//│ = true + +if Pair(0, 1) is Pair(a, b) then true else false +//│ res: bool +//│ = true + + +fun foo(x) = x is Pair(a, b) +//│ foo: anything -> bool +//│ = [Function: foo] + + +Pair(0, 1) is Pair(a, b) and a > b +//│ res: bool +//│ = false + +if Pair(0, 1) is Pair(a, b) then a > b else false +//│ res: bool +//│ = false + + +fun foo(x) = x is Pair(a, b) and a > b +//│ foo: (Pair & {fst: number, snd: number} | ~Pair) -> bool +//│ = [Function: foo1] + +fun foo(x) = if x is Pair(a, b) then a > b else false +//│ foo: (Pair & {fst: number, snd: number} | ~Pair) -> bool +//│ = [Function: foo2] + + + diff --git a/shared/src/test/diff/ucs/SimpleUCS.mls b/shared/src/test/diff/ucs/SimpleUCS.mls new file mode 100644 index 0000000000..b086b248f9 --- /dev/null +++ b/shared/src/test/diff/ucs/SimpleUCS.mls @@ -0,0 +1,388 @@ +:NewParser + +class Option +class Some(value): Option +class None: Option +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Option: () -> Option +//│ = [Function: Option1] +//│ Some: 'value -> (Some with {value: 'value}) +//│ = [Function: Some1] +//│ None: () -> None +//│ = [Function: None1] + +class Either +class Left(leftValue): Either +class Right(rightValue): Either +//│ Defined class Either +//│ Defined class Left +//│ Defined class Right +//│ Either: () -> Either +//│ = [Function: Either1] +//│ Left: 'leftValue -> (Left with {leftValue: 'leftValue}) +//│ = [Function: Left1] +//│ Right: 'rightValue -> (Right with {rightValue: 'rightValue}) +//│ = [Function: Right1] + +:e +:ge +fun f(x, y) = + if x is + Left(xv) and y is Left(yv) then xv + yv + Right(xv) and y is Right(yv) then xv * yv + None() and y is None() then 0 +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.33: Left(xv) and y is Left(yv) then xv + yv +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── The scrutinee at this position misses 2 cases. +//│ ║ l.33: Left(xv) and y is Left(yv) then xv + yv +//│ ║ ^ +//│ ╟── [Missing Case 1/2] `None` +//│ ╟── It first appears here. +//│ ║ l.35: None() and y is None() then 0 +//│ ║ ^^^^^^ +//│ ╟── [Missing Case 2/2] `Right` +//│ ╟── It first appears here. +//│ ║ l.34: Right(xv) and y is Right(yv) then xv * yv +//│ ╙── ^^^^^^^^^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun f(x, y) = + if x is + Left(xv) and y is Left(yv) then xv + yv + None() then 0 +//│ f: (Left & {leftValue: int} | None, Left & {leftValue: int},) -> int +//│ = [Function: f1] + +fun f(x, y) = + if x is + Left(xv) and y is + Left(yv) then xv + yv + Right(yv) then xv * yv + None() then 0 +//│ f: (Left & {leftValue: int} | None, Left & {leftValue: int} | Right & {rightValue: int},) -> int +//│ = [Function: f2] + +fun f(x) = + if x is + Some(v) and + v < 0 then "negative" + v > 0 then "positive" + _ then "zero" + None() then "nothing" +//│ f: (None | Some & {value: number}) -> ("negative" | "nothing" | "positive" | "zero") +//│ = [Function: f3] + +fun f(x, y) = + if x is + Some(x) and y is + Some(y) then 0 +//│ f: (Some, Some,) -> 0 +//│ = [Function: f4] + +class A(value) +class B(value) +//│ Defined class A +//│ Defined class B +//│ A: 'value -> (A with {value: 'value}) +//│ = [Function: A1] +//│ B: 'value -> (B with {value: 'value}) +//│ = [Function: B1] + +fun f(x, y, u, v) = + if x is + A(a) and y == + u then 0 + v then 1 + A(a) and y is + B(0) then 0 + B(1) then 1 + A(_) then 99 +//│ f: (A, number, number, number,) -> (0 | 1 | 99) +//│ = [Function: f5] + +fun f(x) = + if x is + A(_) then "A" + B(_) then "B" +//│ f: (A | B) -> ("A" | "B") +//│ = [Function: f6] + +:e +:ge +fun f(x, y) = + if x is + Some(xv) and y is Some(yv) then xv + yv + None() and y is None() then 0 +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.119: Some(xv) and y is Some(yv) then xv + yv +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.119: Some(xv) and y is Some(yv) then xv + yv +//│ ║ ^ +//│ ╟── [Missing Case 1/1] `None` +//│ ╟── It first appears here. +//│ ║ l.120: None() and y is None() then 0 +//│ ╙── ^^^^^^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun f(x, y) = + if x is + Some(xv) and y is + Some(yv) then xv + yv + None() then xv * 2 + None() and y is + Some(yv) then yv * 3 +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.142: None() and y is +//│ ║ ^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.142: None() and y is +//│ ║ ^ +//│ ╟── [Missing Case 1/1] `None` +//│ ╟── It first appears here. +//│ ║ l.141: None() then xv * 2 +//│ ╙── ^^^^^^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun f(x, y) = + if x is + A and y is + B then "bruh" +//│ f: (A, B,) -> "bruh" +//│ = [Function: f9] + +fun f(x, y, z) = + if x is + A and z == 0 and y == 0 and y is + B then "bruh" + A then "oui" +//│ f: (A, number, number,) -> ("bruh" | "oui") +//│ = [Function: f10] + +// We do need a syntax to specify default branch in IfOpsApp... +:e +:ge +fun f(x, y) = + if x is + Some(x) and y + > 0 then "gt" + < 0 then "le" + == 0 then "eq" +//│ ╔══[ERROR] The case when this is false is not handled: == (y,) (0,) +//│ ║ l.178: Some(x) and y +//│ ║ ^ +//│ ║ l.179: > 0 then "gt" +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.180: < 0 then "le" +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.181: == 0 then "eq" +//│ ╙── ^^^^^^^^^^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun isValid(x) = if x then false else true +//│ isValid: anything -> bool +//│ = [Function: isValid] + +fun f(x, allowNone) = + if x is + Some(x) and isValid(x) then "good" + None() and allowNone then "okay" + else "bad" +//│ f: (anything, anything,) -> ("bad" | "good" | "okay") +//│ = [Function: f12] + +fun f(x) = + if x is + None then "bruh" + Some(x) then "roll" + _ and x == 0 then 0 + _ then "rock" +//│ f: (None | number | Some) -> ("bruh" | "rock" | "roll" | 0) +//│ = [Function: f13] + +fun f(x, a, b) = + if x is + A(aa) and a then aa + B(bb) and b then bb + _ then 0 +//│ f: ((A with {value: 'value}) | (B with {value: 'value}) | ~A & ~B, anything, anything,) -> (0 | 'value) +//│ = [Function: f14] + +fun f(x, y, b) = + if x is + Some(xv) and y + is Some(yv) then "bruh" + is None() then "bruh" + Some(xv) and b then xv + b + _ then "roll" +//│ f: (Some & {value: int} | ~Some, anything, ~true,) -> ("bruh" | "roll" | int) +//│ = [Function: f15] + +fun g(x, y, b) = + if x is + Some(xv) and y + is Some(yv) then yv + is None() then "bruh" + Some(xv) and b then xv + b + _ then "roll" +//│ g: (Some & {value: int} | ~Some, (Some with {value: 'value}) | ~Some, ~true,) -> ("bruh" | "roll" | int | 'value) +//│ = [Function: g] + +fun foo(x, y, z) = + if x - y > 0 then Some(x + y + z) else None() +//│ foo: (int, int, int,) -> (None | Some & {value: int}) +//│ = [Function: foo] + +// Uncomment this block to make the following block work. +// fun foo(x, y, z) = +// if x - y > 0 then Some( +// if x % 2 == 0 then Left(x) else Right(x) +// ) else None() + +:e +fun f(u, v, w) = + if foo(u, v, w) is + Some(x) and x is + Left(_) then "left-defined" + Right(_) then "right-defined" + None then "undefined" +//│ ╔══[ERROR] Type mismatch in `case` expression: +//│ ║ l.257: if foo(u, v, w) is +//│ ║ ^^^^^^^^^^^^^^^ +//│ ║ l.258: Some(x) and x is +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.259: Left(_) then "left-defined" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.260: Right(_) then "right-defined" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.261: None then "undefined" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── operator application of type `int` does not match type `Left & ?a | Right & ?b` +//│ ║ l.245: if x - y > 0 then Some(x + y + z) else None() +//│ ║ ^^^^^^^^^ +//│ ╟── Note: constraint arises from reference: +//│ ║ l.258: Some(x) and x is +//│ ║ ^ +//│ ╟── from application: +//│ ║ l.257: if foo(u, v, w) is +//│ ╙── ^^^^^^^^^^^^ +//│ f: (int, int, int,) -> ("left-defined" | "right-defined" | "undefined") +//│ = [Function: f16] + +fun p(x) = if x >= 0 then Right(x) else Left(x) +//│ p: (number & 'leftValue) -> ((Left with {leftValue: 'leftValue}) | (Right with {rightValue: 'leftValue})) +//│ = [Function: p] + +fun g(a, b) = + if p(a) is + Left(x) and b is + Some(y) then x + y + None then x * a + Right(x) and b is + Some(y) then x * y + None then x +//│ g: (int, None | Some & {value: int},) -> int +//│ = [Function: g1] + +// TODO: Fix the NaN. +g(5, None()) +g(5, Some(7)) +g(0 - 5, None()) +g(0 - 5, Some(9)) +//│ res: int +//│ = 5 +//│ res: int +//│ = 35 +//│ res: int +//│ = NaN +//│ res: int +//│ = 4 + +class Var(name) +class ValBase +class IntVal(value): ValBase +class BoolVal(value): ValBase +class Lit(val) +//│ Defined class Var +//│ Defined class ValBase +//│ Defined class IntVal +//│ Defined class BoolVal +//│ Defined class Lit +//│ Var: 'name -> (Var with {name: 'name}) +//│ = [Function: Var1] +//│ ValBase: () -> ValBase +//│ = [Function: ValBase1] +//│ IntVal: 'value -> (IntVal with {value: 'value}) +//│ = [Function: IntVal1] +//│ BoolVal: 'value -> (BoolVal with {value: 'value}) +//│ = [Function: BoolVal1] +//│ Lit: 'val -> (Lit with {val: 'val}) +//│ = [Function: Lit1] + +fun p(e, context) = + if e is + Var(x) and context.get(x) is + Some(IntVal(v)) then Left(v) + Some(BoolVal(v)) then Right(v) + Lit(IntVal(v)) then Left(v) + Lit(BoolVal(v)) then Right(v) +//│ p: (Lit & {val: (BoolVal with {value: 'rightValue}) | (IntVal with {value: 'leftValue})} | (Var with {name: 'name}), {get: 'name -> (Some & {value: (BoolVal with {value: 'rightValue}) | (IntVal with {value: 'leftValue})})},) -> ((Left with {leftValue: 'leftValue}) | (Right with {rightValue: 'rightValue})) +//│ = [Function: p1] + +class Nil() +//│ Defined class Nil +//│ Nil: () -> Nil +//│ = [Function: Nil1] + +// Support operator constructor like :: +:e +:ge +fun f(x) = + if x is + 0 :: + Nil() then "oh" +//│ ╔══[ERROR] Cannot find operator `::` in the context +//│ ║ l.355: 0 :: +//│ ╙── ^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun f(x) = + if x == 0 and x is + A(_) then "A" + B(_) then "B" + else "bruh" +//│ f: number -> ("A" | "B" | "bruh") +//│ = [Function: f18] + +fun helper(x) = + if x == 0 then None() else Some(x) +//│ helper: (number & 'value) -> (None | (Some with {value: 'value})) +//│ = [Function: helper] + +fun g(x, y) = + if x == 0 and helper(x) is + Some(a) and helper(y) is + Some(b) then a + b + None() then a + 1 + None() and helper(y) is + Some(b) then 2 + b + None() then 1 + else + 0 +//│ g: (int, int,) -> int +//│ = [Function: g2] diff --git a/shared/src/test/diff/ucs/SplitAfterOp.mls b/shared/src/test/diff/ucs/SplitAfterOp.mls new file mode 100644 index 0000000000..37baeee25a --- /dev/null +++ b/shared/src/test/diff/ucs/SplitAfterOp.mls @@ -0,0 +1,164 @@ +:NewParser + +:e +:ge +fun f(x, b) = + if x == + 0 and b then 0 +//│ ╔══[ERROR] The case when this is false is not handled: b +//│ ║ l.7: 0 and b then 0 +//│ ╙── ^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x == y + + 5 then 0 + 7 then 0 +//│ ╔══[ERROR] The case when this is false is not handled: + (== (x,) (y,),) (7,) +//│ ║ l.17: if x == y + +//│ ║ ^^^^^^^^ +//│ ║ l.18: 5 then 0 +//│ ║ ^^^^^^^^^^ +//│ ║ l.19: 7 then 0 +//│ ╙── ^^^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x == y * + 5 then 0 + 6 + 7 then 0 +//│ ╔══[ERROR] The case when this is false is not handled: * (== (x,) (y,),) (+ (6,) (7,),) +//│ ║ l.33: if x == y * +//│ ║ ^^^^^^^^ +//│ ║ l.34: 5 then 0 +//│ ║ ^^^^^^^^^^ +//│ ║ l.35: 6 + 7 then 0 +//│ ╙── ^^^^^^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x == + y + + 5 then 0 + 7 then 0 +//│ ╔══[ERROR] The case when this is false is not handled: + (== (x,) (y,),) (7,) +//│ ║ l.49: if x == +//│ ║ ^^^^ +//│ ║ l.50: y + +//│ ║ ^^^^^ +//│ ║ l.51: 5 then 0 +//│ ║ ^^^^^^^^^^^^ +//│ ║ l.52: 7 then 0 +//│ ╙── ^^^^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x == + 1 and b then 0 +//│ ╔══[ERROR] The case when this is false is not handled: b +//│ ║ l.69: 1 and b then 0 +//│ ╙── ^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + + +:e +:ge +fun toEnglish(x) = + if x == + true then "t" + 0 then "z" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (0,) +//│ ║ l.81: if x == +//│ ║ ^^^^ +//│ ║ l.82: true then "t" +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ║ l.83: 0 then "z" +//│ ╙── ^^^^^^ +//│ toEnglish: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun toEnglish(x) = + if x == + 0 then "z" + true then "t" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (true,) +//│ ║ l.98: if x == +//│ ║ ^^^^ +//│ ║ l.99: 0 then "z" +//│ ║ ^^^^^^^^^^^^^^ +//│ ║ l.100: true then "t" +//│ ╙── ^^^^^^^^ +//│ toEnglish: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun toEnglish(x) = + if x == + 1 then "o" + 0 then "z" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (0,) +//│ ║ l.115: if x == +//│ ║ ^^^^ +//│ ║ l.116: 1 then "o" +//│ ║ ^^^^^^^^^^^^^^ +//│ ║ l.117: 0 then "z" +//│ ╙── ^^^^^^ +//│ toEnglish: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun toEnglish(x) = + if x == + 0 then 1 + else 1 +//│ toEnglish: number -> 1 +//│ = [Function: toEnglish3] + +:pe +:e +:ge +fun toEnglish(x) = + if x == + else 1 +//│ ╔══[PARSE ERROR] Unexpected indented block in expression position +//│ ║ l.141: else 1 +//│ ╙── ^^^^ +//│ ╔══[PARSE ERROR] Unexpected end of indented block; an expression was expected here +//│ ║ l.141: else 1 +//│ ╙── ^ +//│ ╔══[PARSE ERROR] Expected 'then'/'else' clause; found operator application instead +//│ ║ l.140: if x == +//│ ║ ^^^^ +//│ ║ l.141: else 1 +//│ ║ ^^^^ +//│ ╟── Note: 'if' expression started here: +//│ ║ l.140: if x == +//│ ╙── ^^ +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (undefined,) +//│ ║ l.140: if x == +//│ ║ ^^^^ +//│ ║ l.141: else 1 +//│ ╙── ^^^^ +//│ toEnglish: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + diff --git a/shared/src/test/diff/ucs/SplitAnd.mls b/shared/src/test/diff/ucs/SplitAnd.mls new file mode 100644 index 0000000000..5f0dd20e6c --- /dev/null +++ b/shared/src/test/diff/ucs/SplitAnd.mls @@ -0,0 +1,48 @@ +:NewParser + +fun f(x, y) = + if x == 0 and + y == 0 then "bruh" + y == 1 then "lol" + else "okay" +//│ f: (number, number,) -> ("bruh" | "lol" | "okay") +//│ = [Function: f] + +class A() +class B() +//│ Defined class A +//│ Defined class B +//│ A: () -> A +//│ = [Function: A1] +//│ B: () -> B +//│ = [Function: B1] + +:e +:ge +fun f(x) = + if x == 0 and + x is + A() then "A" + B() then "B" + x == 0 then "lol" + else "bruh" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (0,) +//│ ║ l.23: if x == 0 and +//│ ╙── ^^^^^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun f(x, y) = + if + x == 0 and + y == 0 then "bruh" + else "lol" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (0,) +//│ ║ l.40: x == 0 and +//│ ╙── ^^^^^^ +//│ f: (anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared diff --git a/shared/src/test/diff/ucs/SplitAroundOp.mls b/shared/src/test/diff/ucs/SplitAroundOp.mls new file mode 100644 index 0000000000..dc2d1be5b0 --- /dev/null +++ b/shared/src/test/diff/ucs/SplitAroundOp.mls @@ -0,0 +1,119 @@ +:NewParser + +// Why? Can the type of `x` be `number | string`? +:e +fun f(x, b) = + if x + == + 0 and b then "n0" + 1 and b then "n1" + 2 then "n2" + == + "0" then "s0" + "1" then "s1" + "2" then "s2" + else ":p" +//│ ╔══[ERROR] Type mismatch in operator application: +//│ ║ l.6: if x +//│ ║ ^ +//│ ║ l.7: == +//│ ║ ^^^^^^ +//│ ║ l.8: 0 and b then "n0" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.9: 1 and b then "n1" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.10: 2 then "n2" +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ║ l.11: == +//│ ║ ^^^^^^ +//│ ║ l.12: "0" then "s0" +//│ ║ ^^^^^^^^^ +//│ ╟── string literal of type `"0"` is not an instance of type `number` +//│ ║ l.12: "0" then "s0" +//│ ╙── ^^^ +//│ ╔══[ERROR] Type mismatch in operator application: +//│ ║ l.6: if x +//│ ║ ^ +//│ ║ l.7: == +//│ ║ ^^^^^^ +//│ ║ l.8: 0 and b then "n0" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.9: 1 and b then "n1" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.10: 2 then "n2" +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ║ l.11: == +//│ ║ ^^^^^^ +//│ ║ l.12: "0" then "s0" +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ║ l.13: "1" then "s1" +//│ ║ ^^^^^^^^^ +//│ ╟── string literal of type `"1"` is not an instance of type `number` +//│ ║ l.13: "1" then "s1" +//│ ╙── ^^^ +//│ ╔══[ERROR] Type mismatch in operator application: +//│ ║ l.6: if x +//│ ║ ^ +//│ ║ l.7: == +//│ ║ ^^^^^^ +//│ ║ l.8: 0 and b then "n0" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.9: 1 and b then "n1" +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.10: 2 then "n2" +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ║ l.11: == +//│ ║ ^^^^^^ +//│ ║ l.12: "0" then "s0" +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ║ l.13: "1" then "s1" +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ║ l.14: "2" then "s2" +//│ ║ ^^^^^^^^^ +//│ ╟── string literal of type `"2"` is not an instance of type `number` +//│ ║ l.14: "2" then "s2" +//│ ╙── ^^^ +//│ f: (number, anything,) -> (":p" | "n0" | "n1" | "n2" | "s0" | "s1" | "s2") +//│ = [Function: f] + +fun f(x, y, a, b) = + if x == 0 + and + y == 0 then "x, y" + a == 0 then "x, a" + b == 0 then "x, b" + else "nah" +//│ f: (number, number, number, number,) -> ("nah" | "x, a" | "x, b" | "x, y") +//│ = [Function: f1] + +class A() +class B() +//│ Defined class A +//│ Defined class B +//│ A: () -> A +//│ = [Function: A1] +//│ B: () -> B +//│ = [Function: B1] + +fun f(x) = + if x + is + A() then 0 + B() then 1 +//│ f: (A | B) -> (0 | 1) +//│ = [Function: f2] + +// It fails because we interpret == as a constructor. +:e +:ge +if x is + A() + == 0 then 0 + > 0 then 1 + < 0 then 2 +//│ ╔══[ERROR] Cannot find operator `==` in the context +//│ ║ l.111: == 0 then 0 +//│ ╙── ^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared diff --git a/shared/src/test/diff/ucs/SplitBeforeOp.mls b/shared/src/test/diff/ucs/SplitBeforeOp.mls new file mode 100644 index 0000000000..07b7e340a2 --- /dev/null +++ b/shared/src/test/diff/ucs/SplitBeforeOp.mls @@ -0,0 +1,40 @@ +:NewParser + +:e +:ge +if x + == 0 then 0 +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (0,) +//│ ║ l.5: if x +//│ ║ ^ +//│ ║ l.6: == 0 then 0 +//│ ╙── ^^^^^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x + is A and + y then 0 +//│ ╔══[ERROR] Cannot find the constructor `A` in the context +//│ ║ l.19: is A and +//│ ╙── ^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +if x + == 0 then 0 + is + A() then "A" + B() then "B" +//│ ╔══[ERROR] Cannot find class `A` in the context +//│ ║ l.33: A() then "A" +//│ ╙── ^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared diff --git a/shared/src/test/diff/ucs/SplitOps.mls b/shared/src/test/diff/ucs/SplitOps.mls new file mode 100644 index 0000000000..4ef4635e3c --- /dev/null +++ b/shared/src/test/diff/ucs/SplitOps.mls @@ -0,0 +1,130 @@ +:NewParser + +class Option +class Some(value): Option +class None: Option +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Option: () -> Option +//│ = [Function: Option1] +//│ Some: 'value -> (Some with {value: 'value}) +//│ = [Function: Some1] +//│ None: () -> None +//│ = [Function: None1] + +class Either +class Left(leftValue): Either +class Right(rightValue): Either +//│ Defined class Either +//│ Defined class Left +//│ Defined class Right +//│ Either: () -> Either +//│ = [Function: Either1] +//│ Left: 'leftValue -> (Left with {leftValue: 'leftValue}) +//│ = [Function: Left1] +//│ Right: 'rightValue -> (Right with {rightValue: 'rightValue}) +//│ = [Function: Right1] + +:e +:ge +fun f(x) = + if x + is Left(v) then 0 + is Right(v) then 1 + <> undefined then 2 +//│ ╔══[ERROR] The case when this is false is not handled: <> (x,) (undefined,) +//│ ║ l.32: if x +//│ ║ ^ +//│ ║ l.33: is Left(v) then 0 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.34: is Right(v) then 1 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.35: <> undefined then 2 +//│ ╙── ^^^^^^^^^^^^^^^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +:e +:ge +fun f(x) = + if x + is Some(xv) and y is Some(yv) then xv + yv + is None() and y is None() then 0 +//│ ╔══[ERROR] The match is not exhaustive. +//│ ║ l.53: is Some(xv) and y is Some(yv) then xv + yv +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── The scrutinee at this position misses 1 case. +//│ ║ l.53: is Some(xv) and y is Some(yv) then xv + yv +//│ ║ ^ +//│ ╟── [Missing Case 1/1] `None` +//│ ╟── It first appears here. +//│ ║ l.54: is None() and y is None() then 0 +//│ ╙── ^^^^^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +class A() +class B() +//│ Defined class A +//│ Defined class B +//│ A: () -> A +//│ = [Function: A1] +//│ B: () -> B +//│ = [Function: B1] + +fun f(a, b) = + if a + is A() and b is B() then 0 +//│ f: (A, B,) -> 0 +//│ = [Function: f2] + +class C() +//│ Defined class C +//│ C: () -> C +//│ = [Function: C1] + +// * FIXME: the missing otherwise is for `a == 0` +:p +:e +:ge +fun f(a, b, c) = + if a + == 0 and b is B() and c is C() then 0 +//│ |#fun| |f|(|a|,| |b|,| |c|)| |#=|→|#if| |a|→|==| |0| |and| |b| |is| |B|(||)| |and| |c| |is| |C|(||)| |#then| |0|←|←| +//│ Parsed: fun f = a, b, c, => {if a ‹· == (and (and (0,) (is (b,) (B (),),),) (is (c,) (C (),),)) then 0›}; +//│ Desugared: rec def f: a, b, c, => {if a ‹· == (and (and (0,) (is (b,) (B (),),),) (is (c,) (C (),),)) then 0›} +//│ AST: Def(true, f, Lam(Tup(_: Var(a), _: Var(b), _: Var(c)), Blk(...)), true) +//│ ╔══[ERROR] The case when this is false is not handled: and (is (b,) (B (),),) (is (c,) (C (),),) +//│ ║ l.95: == 0 and b is B() and c is C() then 0 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^ +//│ f: (anything, anything, anything,) -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +fun f(x) = + if x + is A() then "A" + is B() then "B" +//│ f: (A | B) -> ("A" | "B") +//│ = [Function: f4] + +fun sumOpt(x, y) = + if x + is Some(xv) and y is + Some(yv) then xv + yv + None() then xv + is None() and y is + Some(yv) then yv + None() then 0 +//│ sumOpt: (None | Some & {value: int}, None | Some & {value: int},) -> int +//│ = [Function: sumOpt] + +fun f(x, y, z) = + if x is A() and y + == z then 1 + is B() then 0 +//│ f: (A, nothing, number,) -> (0 | 1) +//│ = [Function: f5] diff --git a/shared/src/test/diff/ucs/SplitScrutinee.mls b/shared/src/test/diff/ucs/SplitScrutinee.mls new file mode 100644 index 0000000000..77d06ac99f --- /dev/null +++ b/shared/src/test/diff/ucs/SplitScrutinee.mls @@ -0,0 +1,10 @@ +:NewParser +:NoJS + +fun f(x) = + if x + + 1 is + 2 then 1 + 3 then 2 + _ then "I don't know." +//│ f: int -> ("I don't know." | 1 | 2) diff --git a/shared/src/test/diff/ucs/TrivialIf.mls b/shared/src/test/diff/ucs/TrivialIf.mls new file mode 100644 index 0000000000..b2a30947bc --- /dev/null +++ b/shared/src/test/diff/ucs/TrivialIf.mls @@ -0,0 +1,58 @@ +:NewParser +:NoJS + +fun abs(x) = if x < 0 then 0 - x else x +//│ abs: int -> int + +class Option +class Some(value): Option +class None: Option +//│ Defined class Option +//│ Defined class Some +//│ Defined class None +//│ Option: () -> Option +//│ Some: 'value -> (Some with {value: 'value}) +//│ None: () -> None + +fun getOrElse(opt, default) = + if opt is + Some(value) then value + None then default +//│ getOrElse: (None | (Some with {value: 'value}), 'value,) -> 'value + +getOrElse(None(), 0) +//│ res: 0 + +getOrElse(Some(42), 0) +//│ res: 0 | 42 + +fun map(v, f) = + if v is + Some(x) then Some(f(x)) + None then None() +//│ map: (None | (Some with {value: 'value}), 'value -> 'value0,) -> (None | (Some with {value: 'value0})) + +fun inc(x) = x + 5 +//│ inc: int -> int + +map(Some(5), x => x + 5) +//│ res: None | Some & {value: int} + +map(None(), inc) +//│ res: None | Some & {value: int} + +:e +fun f(a, b) = if a and b then 0 +//│ ╔══[ERROR] The case when this is false is not handled: b +//│ ║ l.45: fun f(a, b) = if a and b then 0 +//│ ╙── ^ +//│ f: (anything, anything,) -> error + +:e +fun f(x, y) = + if x == y + 5 then 0 + else if x == y + 7 then 0 +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (+ (y,) (7,),) +//│ ║ l.54: else if x == y + 7 then 0 +//│ ╙── ^^^^^^^^^^ +//│ f: (number, int,) -> (0 | error) diff --git a/shared/src/test/diff/ucs/WeirdIf.mls b/shared/src/test/diff/ucs/WeirdIf.mls new file mode 100644 index 0000000000..46e7bd48df --- /dev/null +++ b/shared/src/test/diff/ucs/WeirdIf.mls @@ -0,0 +1,98 @@ +:NewParser + +// Should report duplicated else branches. +:w +if + _ then 0 + else 0 +else 1 +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ res: 0 +//│ = 0 + +:w +if else 0 else 1 +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ res: 0 +//│ = 0 + +:w +fun f(x) = if x is else 0 else 1 +//│ ╔══[WARNING] duplicated branch +//│ ╙── +//│ f: anything -> 0 +//│ = [Function: f] + +fun f(x) = if x is else 0 +//│ f: anything -> 0 +//│ = [Function: f1] + +:e +:ge +if true + then 0 +//│ ╔══[ERROR] The case when this is false is not handled: true +//│ ║ l.36: if true +//│ ╙── ^^^^ +//│ res: error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +// This cannot be parsed. But the next one works. +:pe +:e +:ge +fun f(x) = + if x == + else "bruh" +//│ ╔══[PARSE ERROR] Unexpected indented block in expression position +//│ ║ l.51: else "bruh" +//│ ╙── ^^^^ +//│ ╔══[PARSE ERROR] Unexpected end of indented block; an expression was expected here +//│ ║ l.51: else "bruh" +//│ ╙── ^ +//│ ╔══[PARSE ERROR] Expected 'then'/'else' clause; found operator application instead +//│ ║ l.50: if x == +//│ ║ ^^^^ +//│ ║ l.51: else "bruh" +//│ ║ ^^^^ +//│ ╟── Note: 'if' expression started here: +//│ ║ l.50: if x == +//│ ╙── ^^ +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (undefined,) +//│ ║ l.50: if x == +//│ ║ ^^^^ +//│ ║ l.51: else "bruh" +//│ ╙── ^^^^ +//│ f: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared + +// But this works. +fun f(x) = + if x == + _ then "bruh" +//│ f: anything -> "bruh" +//│ = [Function: f3] + +:e +:ge +// Hmmmmmm, this one is valid but how to get it work? +fun boolToStr(x) = + if x is + true then "yah" + false then "nah" +//│ ╔══[ERROR] The case when this is false is not handled: == (x,) (false,) +//│ ║ l.86: if x is +//│ ║ ^^^^ +//│ ║ l.87: true then "yah" +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ║ l.88: false then "nah" +//│ ╙── ^^^^^^^^^ +//│ boolToStr: anything -> error +//│ Code generation encountered an error: +//│ if expression has not been desugared diff --git a/shared/src/test/diff/ucs/WeirdSplit.mls b/shared/src/test/diff/ucs/WeirdSplit.mls new file mode 100644 index 0000000000..a41d99f67d --- /dev/null +++ b/shared/src/test/diff/ucs/WeirdSplit.mls @@ -0,0 +1,49 @@ +:NewParser + +class A() +class B() +//│ Defined class A +//│ Defined class B +//│ A: () -> A +//│ = [Function: A1] +//│ B: () -> B +//│ = [Function: B1] + +fun f(x) = + if x + is + A then 0 + B then 1 +//│ f: (A | B) -> (0 | 1) +//│ = [Function: f] + +// Precedence problem: should we restruct terms when push them to the stack? +:e +fun f(x) = + if x == + 1 + + 2 then 0 + + _ then 1 +//│ ╔══[ERROR] Type mismatch in operator application: +//│ ║ l.23: if x == +//│ ║ ^^^^ +//│ ║ l.24: 1 +//│ ║ ^^^^^^ +//│ ║ l.25: + 2 then 0 +//│ ║ ^^^^^^^ +//│ ╟── operator application of type `bool` is not an instance of type `int` +//│ ║ l.23: if x == +//│ ║ ^^^^ +//│ ║ l.24: 1 +//│ ╙── ^^^^^^ +//│ f: number -> (0 | 1) +//│ = [Function: f1] + +fun f(x, s, t) = + if x + is A() + and t then 0 + and s then 0 + is _ then 1 +//│ f: (anything, anything, anything,) -> (0 | 1) +//│ = [Function: f2] diff --git a/shared/src/test/diff/ultimate/Basics.mls b/shared/src/test/diff/ultimate/Basics.mls deleted file mode 100644 index 6d54734931..0000000000 --- a/shared/src/test/diff/ultimate/Basics.mls +++ /dev/null @@ -1,8 +0,0 @@ -:NewParser - - -// TODO -if true then 0 else 1 -//│ /!!!\ Uncaught error: scala.NotImplementedError: an implementation is missing - - diff --git a/shared/src/test/scala/mlscript/CodeGenTestHelpers.scala b/shared/src/test/scala/mlscript/CodeGenTestHelpers.scala new file mode 100644 index 0000000000..a20ce23c24 --- /dev/null +++ b/shared/src/test/scala/mlscript/CodeGenTestHelpers.scala @@ -0,0 +1,90 @@ +package mlscript + +import mlscript.utils._ + +class CodeGenTestHelpers(file: os.Path, output: String => Unit) { + def showGeneratedCode(testCode: JSTestBackend.TestCode): Unit = { + val JSTestBackend.TestCode(prelude, queries) = testCode + if (!prelude.isEmpty) { + output("// Prelude") + prelude foreach { line => + output(line) + } + } + queries.zipWithIndex foreach { + case (JSTestBackend.CodeQuery(prelude, code, _), i) => + output(s"// Query ${i + 1}") + prelude foreach { output(_) } + code foreach { output(_) } + case (JSTestBackend.AbortedQuery(reason), i) => + output(s"// Query ${i + 1} aborted: $reason") + case (JSTestBackend.EmptyQuery, i) => + output(s"// Query ${i + 1} is empty") + } + output("// End of generated code") + } + def showReplPrelude( + sourceLines: List[String], + preludeReply: Option[ReplHost.Reply], + blockLineNum: Int, + ): Unit = { + output(s"┌ Block at ${file.last}:${blockLineNum}") + if (sourceLines.isEmpty) { + output(s"├── No prelude") + } else { + output(s"├─┬ Prelude") + output(s"│ ├── Code") + sourceLines.iterator.foreach { line => output(s"│ │ $line") } + } + preludeReply.foreach { + // Display successful results in multiple lines. + // Display other results in single line. + case ReplHost.Result(content, intermediate) => + intermediate.foreach { value => + output(s"│ ├── Intermediate") + value.linesIterator.foreach { line => output(s"│ │ $line") } + } + output(s"│ └── Reply") + content.linesIterator.foreach { line => output(s"│ $line") } + case other => output(s"│ └── Reply $other") + } + } + def showReplContent( + queries: List[JSTestBackend.Query], + replies: List[ReplHost.Reply], + ): Unit = { + if (queries.isEmpty) { + output(s"└── No queries") + } else { + val length = queries.length // We assume that queries.length === replies.length + queries.iterator.zip(replies).zipWithIndex.foreach { + case ((JSTestBackend.CodeQuery(preludeLines, codeLines, res), reply), i) => + val p0 = if (i + 1 === length) " " else "│ " + val p1 = if (i + 1 === length) "└─" else "├─" + output(s"$p1┬ Query ${i + 1}/${queries.length}") + if (preludeLines.isEmpty) { + output(s"$p0├── Prelude: ") + } else { + output(s"$p0├── Prelude:") + preludeLines.foreach { line => output(s"$p0├── $line") } + } + output(s"$p0├── Code:") + codeLines.foreach { line => output(s"$p0├── $line") } + // Show the intermediate reply if possible. + reply match { + case ReplHost.Result(_, Some(intermediate)) => + output(s"$p0├── Intermediate: $intermediate") + case _ => () + } + val p2 = if (i + 1 === length) " └──" else s"$p0└──" + output(s"$p2 Reply: $reply") + case ((JSTestBackend.AbortedQuery(reason), _), i) => + val prefix = if (i + 1 === queries.length) "└──" else "├──" + output(s"$prefix Query ${i + 1}/${queries.length}: ") + case ((JSTestBackend.EmptyQuery, _), i) => + val prefix = if (i + 1 === queries.length) "└──" else "├──" + output(s"$prefix Query ${i + 1}/${queries.length}: ") + } + } + } +} \ No newline at end of file diff --git a/shared/src/test/scala/mlscript/DiffTests.scala b/shared/src/test/scala/mlscript/DiffTests.scala index b63fbc4a29..3edced5b0a 100644 --- a/shared/src/test/scala/mlscript/DiffTests.scala +++ b/shared/src/test/scala/mlscript/DiffTests.scala @@ -8,10 +8,6 @@ import scala.collection.mutable import scala.collection.mutable.{Map => MutMap} import scala.collection.immutable import mlscript.utils._, shorthands._ -import mlscript.JSTestBackend.IllFormedCode -import mlscript.JSTestBackend.Unimplemented -import mlscript.JSTestBackend.UnexpectedCrash -import mlscript.JSTestBackend.TestCode import mlscript.codegen.typescript.TsTypegenCodeBuilder import org.scalatest.{funsuite, ParallelTestExecution} import org.scalatest.time._ @@ -30,6 +26,7 @@ abstract class ModeType { def dbg: Bool def dbgParsing: Bool def dbgSimplif: Bool + def dbgUCS: Bool def fullExceptionStack: Bool def stats: Bool def stdout: Bool @@ -141,6 +138,7 @@ class DiffTests dbg: Bool = false, dbgParsing: Bool = false, dbgSimplif: Bool = false, + dbgUCS: Bool = false, fullExceptionStack: Bool = false, stats: Bool = false, stdout: Bool = false, @@ -170,6 +168,7 @@ class DiffTests val backend = new JSTestBackend() val host = ReplHost() + val codeGenTestHelpers = new CodeGenTestHelpers(file, output) def rec(lines: List[String], mode: Mode): Unit = lines match { case "" :: Nil => @@ -183,6 +182,7 @@ class DiffTests case "d" => mode.copy(dbg = true) case "dp" => mode.copy(dbgParsing = true) case "ds" => mode.copy(dbgSimplif = true) + case "ducs" => mode.copy(dbg = true, dbgUCS = true) case "s" => mode.copy(fullExceptionStack = true) case "v" | "verbose" => mode.copy(verbose = true) case "ex" | "explain" => mode.copy(expectTypeErrors = true, explainErrors = true) @@ -262,7 +262,7 @@ class DiffTests var totalCodeGenErrors = 0 // report errors and warnings - def report(diags: Ls[mlscript.Diagnostic]): Unit = { + def report(diags: Ls[mlscript.Diagnostic], output: Str => Unit = output): Unit = { diags.foreach { diag => val sctx = Message.mkCtx(diag.allMsgs.iterator.map(_._1), "?") val headStr = diag match { @@ -308,13 +308,14 @@ class DiffTests val pre = s"$shownLineNum: " val curLine = loc.origin.fph.lines(l - 1) output(prepre + pre + "\t" + curLine) - out.print(outputMarker - + (if (isLast && l =:= endLineNum) "╙──" else prepre) + val tickBuilder = new StringBuilder() + tickBuilder ++= ( + (if (isLast && l =:= endLineNum) "╙──" else prepre) + " " * pre.length + "\t" + " " * (c - 1)) val lastCol = if (l =:= endLineNum) endLineCol else curLine.length + 1 - while (c < lastCol) { out.print('^'); c += 1 } - if (c =:= startLineCol) out.print('^') - out.println + while (c < lastCol) { tickBuilder += ('^'); c += 1 } + if (c =:= startLineCol) tickBuilder += ('^') + output(tickBuilder.toString) c = 1 l += 1 } @@ -391,6 +392,7 @@ class DiffTests // if (mode.isDebugging) typer.resetState() if (mode.stats) typer.resetStats() typer.dbg = mode.dbg + typer.dbgUCS = mode.dbgUCS // typer.recordProvenances = !noProvs typer.recordProvenances = !noProvs && !mode.dbg && !mode.dbgSimplif || mode.explainErrors typer.verbose = mode.verbose @@ -524,244 +526,206 @@ class DiffTests }.toList report(diags) } + + // process statements and output mlscript types + // all `Def`s and `Term`s are processed here + // generate typescript types if generateTsDeclarations flag is + // set in the mode + // The tuple type means: (, , , ) + val typerResults: Ls[(Str, Ls[Str], Ls[Str], Bool)] = stmts.map { stmt => + // Because diagnostic lines are after the typing results, + // we need to cache the diagnostic blocks and add them to the + // `typerResults` buffer after the statement has been processed. + val diagnosticLines = mutable.Buffer.empty[Str] + // We put diagnosis to the buffer in the following `Typer` routines. + val raiseToBuffer: typer.Raise = d => { + report(d :: Nil, diagnosticLines += _) + } + // Typing results are before diagnostic messages in the subsumption case. + // We use this flag to prevent too much changes in PR #150. + var typeBeforeDiags = false + val typingResults: Opt[(Str, Ls[Str])] = stmt match { + // statement only declares a new term with its type + // but does not give a body/definition to it + case Def(isrec, nme, R(PolyType(tps, rhs)), isByname) => + typer.dbg = mode.dbg + val ty_sch = typer.PolymorphicType(0, + typer.typeType(rhs)(ctx.nextLevel, raiseToBuffer, + vars = tps.map(tp => tp.name -> typer.freshVar(typer.noProv/*FIXME*/)(1)).toMap)) + ctx += nme.name -> typer.VarSymbol(ty_sch, nme) + declared += nme.name -> ty_sch + val exp = getType(ty_sch) + if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) + S(nme.name -> (s"$nme: ${exp.show}" :: Nil)) - final case class ExecutedResult(var replies: Ls[ReplHost.Reply]) extends JSTestBackend.Result { - def showFirst(prefixLength: Int): Unit = replies match { - case ReplHost.Error(isSyntaxError, content) :: rest => - if (!(mode.expectTypeErrors - || mode.expectRuntimeErrors - || allowRuntimeErrors - || mode.fixme - )) failures += blockLineNum - totalRuntimeErrors += 1 - output((if (isSyntaxError) "Syntax" else "Runtime") + " error:") - content.linesIterator.foreach { s => output(" " + s) } - replies = rest - case ReplHost.Unexecuted(reason) :: rest => - output(" " * prefixLength + "= ") - output(" " * (prefixLength + 2) + reason) - replies = rest - case ReplHost.Result(result, _) :: rest => - result.linesIterator.zipWithIndex.foreach { case (line, i) => - if (i =:= 0) output(" " * prefixLength + "= " + line) - else output(" " * (prefixLength + 2) + line) - } - replies = rest - case ReplHost.Empty :: rest => - output(" " * prefixLength + "= ") - replies = rest - case Nil => () + // statement is defined and has a body/definition + case d @ Def(isrec, nme, L(rhs), isByname) => + typer.dbg = mode.dbg + val ty_sch = typer.typeLetRhs(isrec, nme.name, rhs)(ctx, raiseToBuffer) + val exp = getType(ty_sch) + // statement does not have a declared type for the body + // the inferred type must be used and stored for lookup + S(nme.name -> (declared.get(nme.name) match { + // statement has a body but it's type was not declared + // infer it's type and store it for lookup and type gen + case N => + ctx += nme.name -> typer.VarSymbol(ty_sch, nme) + if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) + s"$nme: ${exp.show}" :: Nil + + // statement has a body and a declared type + // both are used to compute a subsumption (What is this??) + // the inferred type is used to for ts type gen + case S(sign) => + ctx += nme.name -> typer.VarSymbol(sign, nme) + val sign_exp = getType(sign) + typer.dbg = mode.dbg + typer.subsume(ty_sch, sign)(ctx, raiseToBuffer, typer.TypeProvenance(d.toLoc, "def definition")) + if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) + typeBeforeDiags = true + exp.show :: s" <: $nme:" :: sign_exp.show :: Nil + })) + case desug: DesugaredStatement => + typer.dbg = mode.dbg + typer.typeStatement(desug, allowPure = true)(ctx, raiseToBuffer) match { + case R(binds) => + binds.map { case nme -> pty => + val ptType = getType(pty) + ctx += nme -> typer.VarSymbol(pty, Var(nme)) + if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(ptType, Some(nme)) + nme -> (s"$nme: ${ptType.show}" :: Nil) + } + + // statements for terms that compute to a value + // and are not bound to a variable name + case L(pty) => + val exp = getType(pty) + S(if (exp =/= TypeName("unit")) { + val res = "res" + ctx += res -> typer.VarSymbol(pty, Var(res)) + if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, None) + res -> (s"res: ${exp.show}" :: Nil) + } else ( + "" -> Nil + )) + } + } + typingResults match { + case N => ("", Nil, diagnosticLines.toList, false) + case S(name -> typingLines) => + (name, typingLines, diagnosticLines.toList, typeBeforeDiags) } } + + import JSTestBackend._ - var results: JSTestBackend.Result = if (!allowTypeErrors && + val executionResults: Result \/ Ls[ReplHost.Reply] = if (!allowTypeErrors && file.ext =:= "mls" && !mode.noGeneration && !noJavaScript) { + import codeGenTestHelpers._ backend(p, mode.allowEscape) match { - case TestCode(prelude, queries) => { + case testCode @ TestCode(prelude, queries) => { // Display the generated code. - if (mode.showGeneratedJS) { - if (!prelude.isEmpty) { - output("// Prelude") - prelude foreach { line => - output(line) - } - } - queries.zipWithIndex foreach { - case (JSTestBackend.CodeQuery(prelude, code, _), i) => - output(s"// Query ${i + 1}") - prelude foreach { output(_) } - code foreach { output(_) } - case (JSTestBackend.AbortedQuery(reason), i) => - output(s"// Query ${i + 1} aborted: $reason") - case (JSTestBackend.EmptyQuery, i) => - output(s"// Query ${i + 1} is empty") - } - output("// End of generated code") - } + if (mode.showGeneratedJS) showGeneratedCode(testCode) // Execute code. if (!mode.noExecution) { - if (mode.showRepl) { - output(s"┌ Block at ${file.last}:${blockLineNum}") - } - // Execute the prelude code. - prelude match { - case Nil => { - if (mode.showRepl) { - output(s"├── No prelude") - if (queries.isEmpty) - output(s"└── No queries") - } - } - case lines => { - val preludeReply = host.execute(lines mkString " ") - if (mode.showRepl) { - output(s"├─┬ Prelude") - output(s"│ ├── Code") - lines.iterator.foreach { line => output(s"│ │ $line") } - // Display successful results in multiple lines. - // Display other results in single line. - preludeReply match { - case ReplHost.Result(content, intermediate) => - intermediate.foreach { value => - output(s"│ ├── Intermediate") - value.linesIterator.foreach { line => output(s"│ │ $line") } - } - output(s"│ └── Reply") - content.linesIterator.foreach { line => output(s"│ $line") } - case other => output(s"│ └── Reply $other") - } - } - } + val preludeReply = if (prelude.isEmpty) N else S(host.execute(prelude.mkString(" "))) + if (mode.showRepl) showReplPrelude(prelude, preludeReply, blockLineNum) + val replies = queries.map { + case CodeQuery(preludeLines, codeLines, resultName) => + host.query(preludeLines.mkString, codeLines.mkString, resultName) + case AbortedQuery(reason) => ReplHost.Unexecuted(reason) + case EmptyQuery => ReplHost.Empty } - if (mode.showRepl && queries.isEmpty) { - output(s"└── No queries") - } - // Send queries to the host. - ExecutedResult(queries.zipWithIndex.map { - case (JSTestBackend.CodeQuery(preludeLines, codeLines, res), i) => - val prelude = preludeLines.mkString - val code = codeLines.mkString - val p0 = if (i + 1 == queries.length) " " else "│ " - if (mode.showRepl) { - val p1 = if (i + 1 == queries.length) "└─" else "├─" - output(s"$p1┬ Query ${i + 1}/${queries.length}") - output(s"$p0├── Prelude: ${if (preludeLines.isEmpty) "" else prelude}") - output(s"$p0├── Code: ${code}") - } - val reply = host.query(prelude, code, res) - if (mode.showRepl) { - // Show the intermediate reply if possible. - reply match { - case ReplHost.Result(_, Some(intermediate)) => - output(s"$p0├── Intermediate: $intermediate") - case _ => () - } - val p1 = if (i + 1 == queries.length) " └──" else s"$p0└──" - output(s"$p1 Reply: $reply") - } - reply - case (JSTestBackend.AbortedQuery(reason), i) => - if (mode.showRepl) { - val prefix = if (i + 1 == queries.length) "└──" else "├──" - output(s"$prefix Query ${i + 1}/${queries.length}: ") - } - ReplHost.Unexecuted(reason) - case (JSTestBackend.EmptyQuery, i) => - if (mode.showRepl) { - val prefix = if (i + 1 == queries.length) "└──" else "├──" - output(s"$prefix Query ${i + 1}/${queries.length}: ") - } - ReplHost.Empty - }) + if (mode.showRepl) showReplContent(queries, replies) + R(replies) } else { - JSTestBackend.ResultNotExecuted + L(ResultNotExecuted) } } - case t => t + case t => L(t) } } else { - JSTestBackend.ResultNotExecuted - } - - def showFirstResult(prefixLength: Int) = results match { - case t: ExecutedResult => t.showFirst(prefixLength) - case _ => () + L(ResultNotExecuted) } - - // process statements and output mlscript types - // all `Def`s and `Term`s are processed here - // generate typescript types if generateTsDeclarations flag is - // set in the mode - stmts.foreach { - // statement only declares a new term with its type - // but does not give a body/definition to it - case Def(isrec, nme, R(PolyType(tps, rhs)), isByname) => - typer.dbg = mode.dbg - val ty_sch = typer.PolymorphicType(0, - typer.typeType(rhs)(ctx.nextLevel, raise, - vars = tps.map(tp => tp.name -> typer.freshVar(typer.noProv/*FIXME*/)(1)).toMap)) - ctx += nme.name -> ty_sch - declared += nme.name -> ty_sch - val exp = getType(ty_sch) - output(s"$nme: ${exp.show}") - showFirstResult(nme.name.length()) - if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) - - // statement is defined and has a body/definition - case d @ Def(isrec, nme, L(rhs), isByname) => - typer.dbg = mode.dbg - val ty_sch = typer.typeLetRhs(isrec, nme.name, rhs)(ctx, raise) - val exp = getType(ty_sch) - // statement does not have a declared type for the body - // the inferred type must be used and stored for lookup - declared.get(nme.name) match { - // statement has a body but it's type was not declared - // infer it's type and store it for lookup and type gen - case N => - ctx += nme.name -> ty_sch - output(s"$nme: ${exp.show}") - if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) - - // statement has a body and a declared type - // both are used to compute a subsumption (What is this??) - // the inferred type is used to for ts type gen - case S(sign) => - ctx += nme.name -> sign - val sign_exp = getType(sign) - output(exp.show) - output(s" <: $nme:") - output(sign_exp.show) - typer.dbg = mode.dbg - typer.subsume(ty_sch, sign)(ctx, raise, typer.TypeProvenance(d.toLoc, "def definition")) - if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, Some(nme.name)) - } - showFirstResult(nme.name.length()) - case desug: DesugaredStatement => - var prefixLength = 0 - typer.dbg = mode.dbg - typer.typeStatement(desug, allowPure = true)(ctx, raise) match { - // when does this happen?? - case R(binds) => - binds.foreach { - case (nme, pty) => - val ptType = getType(pty) - ctx += nme -> pty - output(s"$nme: ${ptType.show}") - prefixLength = nme.length() - if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(ptType, Some(nme)) - } - // statements for terms that compute to a value - // and are not bound to a variable name - case L(pty) => - val exp = getType(pty) - if (exp =/= TypeName("unit")) { - ctx += "res" -> pty - output(s"res: ${exp.show}") - if (mode.generateTsDeclarations) tsTypegenCodeBuilder.addTypeGenTermDefinition(exp, None) - prefixLength = 3 + // If code generation fails, show the error message. + executionResults match { + case R(replies) => + val replyQueue = mutable.Queue.from(replies) + typerResults.foreach { case (name, typingLines, diagnosticLines, typeBeforeDiags) => + if (typeBeforeDiags) { + typingLines.foreach(output) + diagnosticLines.foreach(output) + } else { + diagnosticLines.foreach(output) + typingLines.foreach(output) + } + val prefixLength = name.length + replyQueue.headOption.foreach { head => + head match { + case ReplHost.Error(isSyntaxError, content) => + // We don't expect type errors nor FIXME. + if (!mode.expectTypeErrors && !mode.fixme) { + // We don't expect code generation errors and it is. + if (!mode.expectCodeGenErrors && isSyntaxError) + failures += blockLineNum + // We don't expect runtime errors and it's a runtime error. + if (!mode.expectRuntimeErrors && !allowRuntimeErrors && !isSyntaxError) + failures += blockLineNum + } + if (isSyntaxError) { + // If there is syntax error in the generated code, + // it should be a code generation error. + output("Syntax error:") + totalCodeGenErrors += 1 + } else { // Otherwise, it is a runtime error. + output("Runtime error:") + totalRuntimeErrors += 1 + } + content.linesIterator.foreach { s => output(" " + s) } + case ReplHost.Unexecuted(reason) => + output(" " * prefixLength + "= ") + output(" " * (prefixLength + 2) + reason) + case ReplHost.Result(result, _) => + result.linesIterator.zipWithIndex.foreach { case (line, i) => + if (i =:= 0) output(" " * prefixLength + "= " + line) + else output(" " * (prefixLength + 2) + line) + } + case ReplHost.Empty => + output(" " * prefixLength + "= ") } + replyQueue.dequeue() + } + } + case L(other) => + // Print type checking results first. + typerResults.foreach { case (_, typingLines, diagnosticLines, typeBeforeDiags) => + if (typeBeforeDiags) { + typingLines.foreach(output) + diagnosticLines.foreach(output) + } else { + diagnosticLines.foreach(output) + typingLines.foreach(output) + } + } + other match { + case _: TestCode => () // Impossible case. + case IllFormedCode(message) => + totalCodeGenErrors += 1 + if (!mode.expectCodeGenErrors && !mode.fixme) + failures += blockLineNum + output("Code generation encountered an error:") + output(s" ${message}") + case Unimplemented(message) => + output("Unable to execute the code:") + output(s" ${message}") + case UnexpectedCrash(name, message) => + if (!mode.fixme) + failures += blockLineNum + output("Code generation crashed:") + output(s" $name: $message") + case ResultNotExecuted => () } - showFirstResult(prefixLength) - } - - // If code generation fails, show the error message. - results match { - case IllFormedCode(message) => - totalCodeGenErrors += 1 - if (!mode.expectCodeGenErrors && !mode.fixme) - failures += blockLineNum - output("Code generation encountered an error:") - output(s" ${message}") - case Unimplemented(message) => - output("Unable to execute the code:") - output(s" ${message}") - case UnexpectedCrash(name, message) => - if (!mode.fixme) - failures += blockLineNum - output("Code generation crashed:") - output(s" $name: $message") - case _ => () } // generate typescript typegen block if (mode.generateTsDeclarations) outputSourceCode(tsTypegenCodeBuilder.toSourceCode()) @@ -852,7 +816,8 @@ object DiffTests { println(" [git] " + gitStr) val prefix = gitStr.take(2) val filePath = os.RelPath(gitStr.drop(3)) - if (prefix =:= "A " || prefix =:= "M ") N else S(filePath) // disregard modified files that are staged + if (prefix =:= "A " || prefix =:= "M " || prefix =:= "R " || prefix =:= "D ") N // disregard modified files that are staged + else S(filePath) }.toSet catch { case err: Throwable => System.err.println("/!\\ git command failed with: " + err) Set.empty