From 8bf939bd91842d6c6f509a6d5c408f194b130464 Mon Sep 17 00:00:00 2001 From: "P. Oscar Boykin" Date: Sun, 8 Oct 2023 07:32:44 -1000 Subject: [PATCH] Char literal and patterns (#1052) * Add parsing of char patterns * checkpoint mostly working * checkpoint with interpreter tests working * hopefully get tests green * make scalajs tests pass * fix python SelectItem generation * avoid codepoints for scalajs * increase coverage * actually add LitTest file * improve Lit.fromChar test * Implement char matching in Python * fix python test --- cli/src/main/protobuf/bosatsu/TypedAst.proto | 3 + .../org/bykn/bosatsu/TypedExprToProto.scala | 12 ++ .../src/main/resources/bosatsu/predef.bosatsu | 4 + .../scala/org/bykn/bosatsu/Declaration.scala | 58 ++++--- .../org/bykn/bosatsu/DefRecursionCheck.scala | 5 +- .../src/main/scala/org/bykn/bosatsu/Lit.scala | 47 +++++- .../scala/org/bykn/bosatsu/Matchless.scala | 6 + .../org/bykn/bosatsu/MatchlessToValue.scala | 40 ++++- .../scala/org/bykn/bosatsu/PackageError.scala | 2 +- .../main/scala/org/bykn/bosatsu/Pattern.scala | 82 +++++---- .../main/scala/org/bykn/bosatsu/Predef.scala | 4 + .../org/bykn/bosatsu/SourceConverter.scala | 40 +++-- .../scala/org/bykn/bosatsu/StringUtil.scala | 69 ++++++-- .../org/bykn/bosatsu/TotalityCheck.scala | 14 +- .../bykn/bosatsu/TypedExprNormalization.scala | 3 +- .../main/scala/org/bykn/bosatsu/Value.scala | 1 + .../bykn/bosatsu/codegen/python/Code.scala | 58 +++++-- .../bosatsu/codegen/python/PythonGen.scala | 158 +++++++++++------- .../org/bykn/bosatsu/pattern/SeqPattern.scala | 6 +- .../scala/org/bykn/bosatsu/rankn/Infer.scala | 1 + .../scala/org/bykn/bosatsu/rankn/Type.scala | 3 + .../org/bykn/bosatsu/EvaluationTest.scala | 31 +++- .../src/test/scala/org/bykn/bosatsu/Gen.scala | 32 +++- .../test/scala/org/bykn/bosatsu/LitTest.scala | 62 +++++++ .../scala/org/bykn/bosatsu/ParserTest.scala | 32 +++- .../scala/org/bykn/bosatsu/TotalityTest.scala | 24 ++- .../bykn/bosatsu/pattern/SeqPatternTest.scala | 6 + test_workspace/Char.bosatsu | 52 ++++++ 28 files changed, 676 insertions(+), 179 deletions(-) create mode 100644 core/src/test/scala/org/bykn/bosatsu/LitTest.scala create mode 100644 test_workspace/Char.bosatsu diff --git a/cli/src/main/protobuf/bosatsu/TypedAst.proto b/cli/src/main/protobuf/bosatsu/TypedAst.proto index 2cbffbe6e..4e240bdb9 100644 --- a/cli/src/main/protobuf/bosatsu/TypedAst.proto +++ b/cli/src/main/protobuf/bosatsu/TypedAst.proto @@ -194,6 +194,7 @@ message Literal { string stringValue = 1; int64 intValueAs64 = 2; string intValueAsString = 3; + int32 charValue = 4; } } @@ -221,6 +222,8 @@ message StrPart { WildCardPat unnamedStr = 1; int32 namedStr = 2; int32 literalStr = 3; + WildCardPat unnamedChar = 4; + int32 namedChar = 5; } } diff --git a/cli/src/main/scala/org/bykn/bosatsu/TypedExprToProto.scala b/cli/src/main/scala/org/bykn/bosatsu/TypedExprToProto.scala index 0bc64af88..4b3e54c3e 100644 --- a/cli/src/main/scala/org/bykn/bosatsu/TypedExprToProto.scala +++ b/cli/src/main/scala/org/bykn/bosatsu/TypedExprToProto.scala @@ -306,6 +306,8 @@ object ProtoConverter { case proto.StrPart.Value.LiteralStr(idx) => str(idx).map(Pattern.StrPart.LitStr(_)) case proto.StrPart.Value.UnnamedStr(_) => Success(Pattern.StrPart.WildStr) case proto.StrPart.Value.NamedStr(idx) => bindable(idx).map { n => Pattern.StrPart.NamedStr(n) } + case proto.StrPart.Value.UnnamedChar(_) => Success(Pattern.StrPart.WildChar) + case proto.StrPart.Value.NamedChar(idx) => bindable(idx).map { n => Pattern.StrPart.NamedChar(n) } } items.toList match { @@ -542,6 +544,8 @@ object ProtoConverter { case _: ArithmeticException => proto.Literal.Value.IntValueAsString(i.toString) } + case c @ Lit.Chr(_) => + proto.Literal.Value.CharValue(c.toCodePoint) case Lit.Str(str) => proto.Literal.Value.StringValue(str) } @@ -554,6 +558,8 @@ object ProtoConverter { Failure(new Exception("unexpected unset Literal value in pattern")) case proto.Literal.Value.StringValue(s) => Success(Lit.Str(s)) + case proto.Literal.Value.CharValue(cp) => + Success(Lit.Chr.fromCodePoint(cp)) case proto.Literal.Value.IntValueAs64(l) => Success(Lit(l)) case proto.Literal.Value.IntValueAsString(s) => @@ -587,10 +593,16 @@ object ProtoConverter { parts.traverse { case Pattern.StrPart.WildStr => tabPure(proto.StrPart(proto.StrPart.Value.UnnamedStr(proto.WildCardPat()))) + case Pattern.StrPart.WildChar => + tabPure(proto.StrPart(proto.StrPart.Value.UnnamedChar(proto.WildCardPat()))) case Pattern.StrPart.NamedStr(n) => getId(n.sourceCodeRepr).map { idx => proto.StrPart(proto.StrPart.Value.NamedStr(idx)) } + case Pattern.StrPart.NamedChar(n) => + getId(n.sourceCodeRepr).map { idx => + proto.StrPart(proto.StrPart.Value.NamedChar(idx)) + } case Pattern.StrPart.LitStr(s) => getId(s).map { idx => proto.StrPart(proto.StrPart.Value.LiteralStr(idx)) diff --git a/core/src/main/resources/bosatsu/predef.bosatsu b/core/src/main/resources/bosatsu/predef.bosatsu index 48cd4a99f..6fccbf1d7 100644 --- a/core/src/main/resources/bosatsu/predef.bosatsu +++ b/core/src/main/resources/bosatsu/predef.bosatsu @@ -2,6 +2,7 @@ package Bosatsu/Predef export ( Bool(), + Char, Comparison(), Int, Option(), @@ -14,6 +15,7 @@ export ( Dict, add, add_key, + char_to_String, cmp_Int, concat, concat_String, @@ -151,7 +153,9 @@ def range_fold(inclusiveLower: Int, exclusiveUpper: Int, init: a, fn: (a, Int) - # String functions ############# external struct String +external struct Char +external def char_to_String(c: Char) -> String external def string_Order_fn(str0: String, str1: String) -> Comparison string_Order = Order(string_Order_fn) external def concat_String(items: List[String]) -> String diff --git a/core/src/main/scala/org/bykn/bosatsu/Declaration.scala b/core/src/main/scala/org/bykn/bosatsu/Declaration.scala index 8c81bf11c..031213b6f 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Declaration.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Declaration.scala @@ -146,14 +146,15 @@ sealed abstract class Declaration { case StringDecl(parts) => val useDouble = parts.exists { - case Right((_, str)) => str.contains('\'') && !str.contains('"') - case Left(_) => false + case StringDecl.Literal(_, str) => str.contains('\'') && !str.contains('"') + case _ => false } val q = if (useDouble) '"' else '\'' val inner = Doc.intercalate(Doc.empty, parts.toList.map { - case Right((_, str)) => Doc.text(StringUtil.escape(q, str)) - case Left(decl) => Doc.text("${") + decl.toDoc + Doc.char('}') + case StringDecl.Literal(_, str) => Doc.text(StringUtil.escape(q, str)) + case StringDecl.StrExpr(decl) => Doc.text("${") + decl.toDoc + Doc.char('}') + case StringDecl.CharExpr(decl) => Doc.text("$.{") + decl.toDoc + Doc.char('}') }) Doc.char(q) + inner + Doc.char(q) @@ -233,7 +234,8 @@ sealed abstract class Declaration { case Var(_) => acc case StringDecl(items) => items.foldLeft(acc) { - case (acc, Left(nb)) => loop(nb, bound, acc) + case (acc, StringDecl.StrExpr(nb)) => loop(nb, bound, acc) + case (acc, StringDecl.CharExpr(nb)) => loop(nb, bound, acc) case (acc, _) => acc } case ListDecl(ListLang.Cons(items)) => @@ -345,8 +347,9 @@ sealed abstract class Declaration { case Var(_) => acc case StringDecl(nel) => nel.foldLeft(acc) { - case (acc0, Left(decl)) => loop(decl, acc0) - case (acc0, Right(_)) => acc0 + case (acc0, StringDecl.StrExpr(decl)) => loop(decl, acc0) + case (acc0, StringDecl.CharExpr(decl)) => loop(decl, acc0) + case (acc0, _) => acc0 } case ListDecl(ListLang.Cons(items)) => items.foldLeft(acc) { (acc0, sori) => @@ -523,8 +526,9 @@ object Declaration { case StringDecl(nel) => nel .traverse { - case Left(nb) => loop(nb).map(Left(_)) - case right => Some(right) + case StringDecl.StrExpr(nb) => loop(nb).map(StringDecl.StrExpr(_)) + case StringDecl.CharExpr(nb) => loop(nb).map(StringDecl.CharExpr(_)) + case lit => Some(lit) } .map(StringDecl(_)(decl.region)) case ListDecl(ll) => @@ -669,8 +673,9 @@ object Declaration { case Var(b) => Var(b)(r) case StringDecl(nel) => val ne1 = nel.map { - case Right((_, s)) => Right((r, s)) - case Left(e) => Left(e.replaceRegionsNB(r)) + case StringDecl.Literal(_, s) => StringDecl.Literal(r, s) + case StringDecl.CharExpr(e) => StringDecl.CharExpr(e.replaceRegionsNB(r)) + case StringDecl.StrExpr(e) => StringDecl.StrExpr(e.replaceRegionsNB(r)) } StringDecl(ne1)(r) case ListDecl(ListLang.Cons(items)) => @@ -756,7 +761,13 @@ object Declaration { /** * This represents interpolated strings */ - case class StringDecl(items: NonEmptyList[Either[NonBinding, (Region, String)]])(implicit val region: Region) extends NonBinding + case class StringDecl(items: NonEmptyList[StringDecl.Part])(implicit val region: Region) extends NonBinding + object StringDecl { + sealed abstract class Part + case class Literal(region: Region, toStr: String) extends Part + case class StrExpr(nonBinding: NonBinding) extends Part + case class CharExpr(nonBinding: NonBinding) extends Part + } /** * This represents the list construction language */ @@ -788,13 +799,14 @@ object Declaration { Pattern.StructKind.Named(nm, Pattern.StructKind.Style.TupleLike), Nil)) case Var(v: Bindable) => Some(Pattern.Var(v)) case Literal(lit) => Some(Pattern.Literal(lit)) - case StringDecl(NonEmptyList(Right((_, s)), Nil)) => + case StringDecl(NonEmptyList(StringDecl.Literal(_, s), Nil)) => Some(Pattern.Literal(Lit.Str(s))) case StringDecl(items) => - def toStrPart(p: Either[NonBinding, (Region, String)]): Option[Pattern.StrPart] = + def toStrPart(p: StringDecl.Part): Option[Pattern.StrPart] = p match { - case Right((_, str)) => Some(Pattern.StrPart.LitStr(str)) - case Left(Var(v: Bindable)) => Some(Pattern.StrPart.NamedStr(v)) + case StringDecl.Literal(_, str) => Some(Pattern.StrPart.LitStr(str)) + case StringDecl.StrExpr(Var(v: Bindable)) => Some(Pattern.StrPart.NamedStr(v)) + case StringDecl.CharExpr(Var(v: Bindable)) => Some(Pattern.StrPart.NamedChar(v)) case _ => None } items.traverse(toStrPart).map(Pattern.StrPat(_)) @@ -913,7 +925,8 @@ object Declaration { } def stringDeclOrLit(inner: Indy[NonBinding]): Indy[NonBinding] = { - val start = P.string("${") + val start = P.string("${").as((a: NonBinding) => StringDecl.StrExpr(a)) | + P.string("$.{").as((a: NonBinding) => StringDecl.CharExpr(a)) val end = P.char('}') val q1 = '\'' val q2 = '"' @@ -929,7 +942,10 @@ object Declaration { case (r, Right((_, str)) :: Nil) => Literal(Lit.Str(str))(r) case (r, h :: tail) => - StringDecl(NonEmptyList(h, tail))(r) + StringDecl(NonEmptyList(h, tail).map { + case Right((region, str)) => StringDecl.Literal(region, str) + case Left(expr) => expr + })(r) } } } @@ -1069,7 +1085,8 @@ object Declaration { .region .map { case (r, l) => DictDecl(l)(r) } - val lits: P[Literal] = Lit.integerParser.region.map { case (r, l) => Literal(l)(r) } + val lits: P[Literal] = + (Lit.integerParser | Lit.codePointParser).region.map { case (r, l) => Literal(l)(r) } private sealed abstract class ParseMode private object ParseMode { @@ -1171,8 +1188,9 @@ object Declaration { val slashcontinuation = ((maybeSpace ~ P.char('\\') ~ toEOL1).backtrack ~ Parser.maybeSpacesAndLines).?.void // 0 or more args val params0 = recNonBind.parensLines0Cut + val justDot = P.not(P.string(".\"") | P.string(".'")).with1 *> P.char('.') val dotApply: P[NonBinding => NonBinding] = - (slashcontinuation.with1 *> P.char('.') *> (fn ~ params0)) + (slashcontinuation.with1 *> justDot *> (fn ~ params0)) .region .map { case (r2, (fn, args)) => diff --git a/core/src/main/scala/org/bykn/bosatsu/DefRecursionCheck.scala b/core/src/main/scala/org/bykn/bosatsu/DefRecursionCheck.scala index a8c76e5c1..7fbdd83e6 100644 --- a/core/src/main/scala/org/bykn/bosatsu/DefRecursionCheck.scala +++ b/core/src/main/scala/org/bykn/bosatsu/DefRecursionCheck.scala @@ -522,8 +522,9 @@ object DefRecursionCheck { } case StringDecl(parts) => parts.parTraverse_ { - case Left(nb) => checkDecl(nb) - case Right(_) => unitSt + case StringDecl.CharExpr(nb) => checkDecl(nb) + case StringDecl.StrExpr(nb) => checkDecl(nb) + case StringDecl.Literal(_, _) => unitSt } case ListDecl(ll) => ll match { diff --git a/core/src/main/scala/org/bykn/bosatsu/Lit.scala b/core/src/main/scala/org/bykn/bosatsu/Lit.scala index 2213f515e..593635b3e 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Lit.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Lit.scala @@ -10,6 +10,8 @@ sealed abstract class Lit { def repr: String = this match { case Lit.Integer(i) => i.toString + case c @ Lit.Chr(_) => + ".'" + escape('\'', c.asStr) + "'" case Lit.Str(s) => "\"" + escape('"', s) + "\"" } @@ -22,10 +24,35 @@ object Lit { case class Str(toStr: String) extends Lit { def unboxToAny: Any = toStr } + case class Chr(asStr: String) extends Lit { + def toCodePoint: Int = asStr.codePointAt(0) + def unboxToAny: Any = asStr + } + object Chr { + private def build(cp: Int): Chr = + Chr((new java.lang.StringBuilder).appendCodePoint(cp).toString) + + private[this] val cache: Array[Chr] = + (0 until 256).map(build).toArray + /** + * @throws IllegalArgumentException on a bad codepoint + */ + def fromCodePoint(cp: Int): Chr = + if ((0 <= cp) && (cp < 256)) cache(cp) + else build(cp) + } val EmptyStr: Str = Str("") def fromInt(i: Int): Lit = Integer(BigInteger.valueOf(i.toLong)) + + def fromChar(c: Char): Lit = + if (0xd800 <= c && c < 0xe000) + throw new IllegalArgumentException(s"utf-16 character int=${c.toInt} is not a valid single codepoint") + else Chr.fromCodePoint(c.toInt) + + def fromCodePoint(cp: Int): Lit = Chr.fromCodePoint(cp) + def apply(i: Long): Lit = apply(BigInteger.valueOf(i)) def apply(bi: BigInteger): Lit = Integer(bi) def apply(str: String): Lit = Str(str) @@ -42,18 +69,26 @@ object Lit { str(q1).orElse(str(q2)) } + val codePointParser: P[Chr] = { + (StringUtil.codepoint(P.string(".\""), P.char('"')) | + StringUtil.codepoint(P.string(".'"), P.char('\''))).map(Chr.fromCodePoint(_)) + } + implicit val litOrdering: Ordering[Lit] = new Ordering[Lit] { def compare(a: Lit, b: Lit): Int = (a, b) match { case (Integer(a), Integer(b)) => a.compareTo(b) - case (Integer(_), Str(_)) => -1 - case (Str(_), Integer(_)) => 1 + case (Integer(_), Str(_) | Chr(_)) => -1 + case (Chr(_), Integer(_)) => 1 + case (Chr(a), Chr(b)) => a.compareTo(b) + case (Chr(_), Str(_)) => -1 + case (Str(_), Integer(_)| Chr(_)) => 1 case (Str(a), Str(b)) => a.compareTo(b) } } - val parser: P[Lit] = integerParser.orElse(stringParser) + val parser: P[Lit] = integerParser | stringParser | codePointParser implicit val document: Document[Lit] = Document.instance[Lit] { @@ -62,6 +97,12 @@ object Lit { case Str(str) => val q = if (str.contains('\'') && !str.contains('"')) '"' else '\'' Doc.char(q) + Doc.text(escape(q, str)) + Doc.char(q) + case c @ Chr(_) => + val str = c.asStr + val (start, end) = + if (str.contains('\'') && !str.contains('"')) (".\"", '"') + else (".'", '\'') + Doc.text(start) + Doc.text(escape(end, str)) + Doc.char(end) } } diff --git a/core/src/main/scala/org/bykn/bosatsu/Matchless.scala b/core/src/main/scala/org/bykn/bosatsu/Matchless.scala index 28c65444c..f4a2f94e9 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Matchless.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Matchless.scala @@ -18,8 +18,11 @@ object Matchless { sealed abstract class StrPart object StrPart { sealed abstract class Glob(val capture: Boolean) extends StrPart + sealed abstract class CharPart(val capture: Boolean) extends StrPart case object WildStr extends Glob(false) case object IndexStr extends Glob(true) + case object WildChar extends CharPart(false) + case object IndexChar extends CharPart(true) case class LitStr(asString: String) extends StrPart } @@ -341,13 +344,16 @@ object Matchless { // that each name is distinct // should be checked in the SourceConverter/TotalityChecking code case Pattern.StrPart.NamedStr(n) => n + case Pattern.StrPart.NamedChar(n) => n } val muts = sbinds.traverse { b => makeAnon.map(LocalAnonMut(_)).map((b, _)) } val pat = items.toList.map { case Pattern.StrPart.NamedStr(_) => StrPart.IndexStr + case Pattern.StrPart.NamedChar(_) => StrPart.IndexChar case Pattern.StrPart.WildStr => StrPart.WildStr + case Pattern.StrPart.WildChar => StrPart.WildChar case Pattern.StrPart.LitStr(s) => StrPart.LitStr(s) } diff --git a/core/src/main/scala/org/bykn/bosatsu/MatchlessToValue.scala b/core/src/main/scala/org/bykn/bosatsu/MatchlessToValue.scala index 93f099b44..30abbbc1e 100644 --- a/core/src/main/scala/org/bykn/bosatsu/MatchlessToValue.scala +++ b/core/src/main/scala/org/bykn/bosatsu/MatchlessToValue.scala @@ -533,14 +533,30 @@ object MatchlessToValue { def matchString(str: String, pat: List[Matchless.StrPart], binds: Int): Array[String] = { import Matchless.StrPart._ + val strLen = str.length() val results = if (binds > 0) new Array[String](binds) else emptyStringArray def loop(offset: Int, pat: List[Matchless.StrPart], next: Int): Boolean = pat match { - case Nil => offset == str.length + case Nil => offset == strLen case LitStr(expect) :: tail => val len = expect.length str.regionMatches(offset, expect, 0, len) && loop(offset + len, tail, next) + case (c: CharPart) :: tail => + try { + val nextOffset = str.offsetByCodePoints(offset, 1) + val n = + if (c.capture) { + results(next) = str.substring(offset, nextOffset) + next + 1 + } + else next + + loop(nextOffset, tail, n) + } + catch { + case _: IndexOutOfBoundsException => false + } case (h: Glob) :: tail => tail match { case Nil => @@ -549,6 +565,28 @@ object MatchlessToValue { results(next) = str.substring(offset) } true + case rest @ ((_: CharPart) :: _) => + // (.*)(.)tail2 + // this is a naive algorithm that just + // checks at all possible later offsets + // a smarter algorithm could see if there + // are Lit parts that can match or not + var matched = false + var off1 = offset + val n1 = if (h.capture) (next + 1) else next + while (!matched && (off1 < strLen)) { + matched = loop(off1, rest, n1) + if (!matched) { + off1 = off1 + 1 + } + } + + matched && { + if (h.capture) { + results(next) = str.substring(offset, off1) + } + true + } case LitStr(expect) :: tail2 => val next1 = if (h.capture) next + 1 else next diff --git a/core/src/main/scala/org/bykn/bosatsu/PackageError.scala b/core/src/main/scala/org/bykn/bosatsu/PackageError.scala index c1c1aa460..86caa6488 100644 --- a/core/src/main/scala/org/bykn/bosatsu/PackageError.scala +++ b/core/src/main/scala/org/bykn/bosatsu/PackageError.scala @@ -485,7 +485,7 @@ object PackageError { case InvalidStrPat(pat, _) => Doc.text(s"invalid string pattern: ") + Document[Pattern.Parsed].document(pat) + - Doc.text(" (adjacent bindings aren't allowed)") + Doc.text(" (adjacent string bindings aren't allowed)") case MultipleSplicesInPattern(_, _) => // TODO: get printing of compiled patterns working well //val docp = Document[Pattern.Parsed].document(Pattern.ListPat(pat)) + diff --git a/core/src/main/scala/org/bykn/bosatsu/Pattern.scala b/core/src/main/scala/org/bykn/bosatsu/Pattern.scala index e79c11175..1f3b7eaee 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Pattern.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Pattern.scala @@ -37,7 +37,10 @@ sealed abstract class Pattern[+N, +T] { if (seen(v)) loop(p :: tail, seen, acc) else loop(p :: tail, seen + v, v :: acc) case Pattern.StrPat(items) :: tail => - val names = items.collect { case Pattern.StrPart.NamedStr(n) => n }.filterNot(seen) + val names = items.collect { + case Pattern.StrPart.NamedStr(n) => n + case Pattern.StrPart.NamedChar(n) => n + }.filterNot(seen) loop(tail, seen ++ names, names reverse_::: acc) case Pattern.ListPat(items) :: tail => val globs = items.collect { case Pattern.ListPart.NamedList(glob) => glob }.filterNot(seen) @@ -144,10 +147,13 @@ sealed abstract class Pattern[+N, +T] { else inner case Pattern.StrPat(items) => Pattern.StrPat(items.map { - case wl@(Pattern.StrPart.WildStr | Pattern.StrPart.LitStr(_)) => wl + case wl@(Pattern.StrPart.WildStr | Pattern.StrPart.WildChar | Pattern.StrPart.LitStr(_)) => wl case in@Pattern.StrPart.NamedStr(n) => if (keep(n)) in else Pattern.StrPart.WildStr + case in@Pattern.StrPart.NamedChar(n) => + if (keep(n)) in + else Pattern.StrPart.WildChar }) case Pattern.ListPat(items) => Pattern.ListPat(items.map { @@ -182,10 +188,13 @@ sealed abstract class Pattern[+N, +T] { else (s1 + v, l1) case Pattern.StrPat(items) => items.foldLeft((Set.empty[Bindable], List.empty[Bindable])) { - case (res, Pattern.StrPart.WildStr | Pattern.StrPart.LitStr(_)) => res + case (res, Pattern.StrPart.WildStr | Pattern.StrPart.WildChar | Pattern.StrPart.LitStr(_)) => res case ((s1, l1), Pattern.StrPart.NamedStr(v)) => if (s1(v)) (s1, v :: l1) else (s1 + v, l1) + case ((s1, l1), Pattern.StrPart.NamedChar(v)) => + if (s1(v)) (s1, v :: l1) + else (s1 + v, l1) } case Pattern.ListPat(items) => items.foldLeft((Set.empty[Bindable], List.empty[Bindable])) { @@ -274,18 +283,24 @@ object Pattern { object StrPart { final case object WildStr extends StrPart final case class NamedStr(name: Bindable) extends StrPart + final case object WildChar extends StrPart + final case class NamedChar(name: Bindable) extends StrPart final case class LitStr(asString: String) extends StrPart // this is to circumvent scala warnings because these bosatsu // patterns like right. private[this] val dollar = "$" private[this] val wildDoc = Doc.text(s"$dollar{_}") + private[this] val wildCharDoc = Doc.text(s"${dollar}.{_}") private[this] val prefix = Doc.text(s"$dollar{") + private[this] val prefixChar = Doc.text(s"${dollar}.{") def document(q: Char): Document[StrPart] = Document.instance { case WildStr => wildDoc + case WildChar => wildCharDoc case NamedStr(b) => prefix + Document[Bindable].document(b) + Doc.char('}') + case NamedChar(b) => prefixChar + Document[Bindable].document(b) + Doc.char('}') case LitStr(s) => Doc.text(StringUtil.escape(q, s)) } } @@ -510,11 +525,9 @@ object Pattern { case SeqPart.Lit(c) :: tail => loop(tail, c :: front) case SeqPart.AnyElem :: tail => - // TODO, it would be nice to support AnyElem directly - // in our string pattern language, but for now, we add wild - val tailRes = loop(tail, Nil) - if (tailRes.head == StrPart.WildStr) tailRes - else tailRes.prepend(StrPart.WildStr) + loop(tail, Nil) + .prepend(StrPart.WildChar) + .prependList(lit(front)) case SeqPart.Wildcard :: SeqPart.AnyElem :: tail => // *_, _ is the same as _, *_ loop(SeqPart.AnyElem :: SeqPart.Wildcard :: tail, front) @@ -531,30 +544,26 @@ object Pattern { } def toNamedSeqPattern(sp: StrPat): NamedSeqPattern[Char] = { + val empty: NamedSeqPattern[Char] = NamedSeqPattern.NEmpty + def partToNsp(s: StrPart): NamedSeqPattern[Char] = s match { case StrPart.NamedStr(n) => NamedSeqPattern.Bind(n.sourceCodeRepr, NamedSeqPattern.Wild) + case StrPart.NamedChar(n) => + NamedSeqPattern.Bind(n.sourceCodeRepr, NamedSeqPattern.Any) case StrPart.WildStr => NamedSeqPattern.Wild + case StrPart.WildChar => NamedSeqPattern.Any case StrPart.LitStr(s) => - // reverse so we can build right associated - s.toList.reverse match { - case Nil => NamedSeqPattern.NEmpty - case h :: tail => - tail.foldLeft(NamedSeqPattern.fromLit(h)) { (right, head) => - NamedSeqPattern.NCat(NamedSeqPattern.fromLit(head), right) - } + if (s.isEmpty) empty + else s.toList.foldRight(empty) { (c, tail) => + NamedSeqPattern.NCat(NamedSeqPattern.fromLit(c), tail) } } - def loop(sp: List[StrPart]): NamedSeqPattern[Char] = - sp match { - case Nil => NamedSeqPattern.NEmpty - case h :: t => - NamedSeqPattern.NCat(partToNsp(h), loop(t)) - } - - loop(sp.parts.toList) + sp.parts.toList.foldRight(empty) { (h, t) => + NamedSeqPattern.NCat(partToNsp(h), t) + } } def fromLitStr(s: String): StrPat = @@ -615,9 +624,15 @@ object Pattern { (a, b) match { case (WildStr, WildStr) => 0 case (WildStr, _) => -1 - case (LitStr(_), WildStr) => 1 + case (WildChar, WildStr) => 1 + case (WildChar, WildChar) => 0 + case (WildChar, _) => -1 + case (LitStr(_), WildStr | WildChar) => 1 case (LitStr(sa), LitStr(sb)) => sa.compareTo(sb) - case (LitStr(_), NamedStr(_)) => -1 + case (LitStr(_), NamedStr(_) | NamedChar(_)) => -1 + case (NamedChar(_), WildStr | WildChar | LitStr(_)) => 1 + case (NamedChar(na), NamedChar(nb)) => ordBin.compare(na, nb) + case (NamedChar(_), NamedStr(_)) => -1 case (NamedStr(na), NamedStr(nb)) => ordBin.compare(na, nb) case (NamedStr(_), _) => 1 } @@ -905,15 +920,22 @@ object Pattern { private[this] val pwild = P.char('_').as(WildCard) private[this] val plit: P[Pattern[Nothing, Nothing]] = { - val intp = Lit.integerParser.map(Literal(_)) - val start = P.string("${") + val intp = (Lit.integerParser | Lit.codePointParser).map(Literal(_)) + val startStr = P.string("${").as { (opt: Option[Bindable]) => + opt.fold(StrPart.WildStr: StrPart)(StrPart.NamedStr(_)) + } + val startChar = P.string("$.{").as { (opt: Option[Bindable]) => + opt.fold(StrPart.WildChar: StrPart)(StrPart.NamedChar(_)) + } + val start = startStr | startChar val end = P.char('}') - val pwild = P.char('_').as(StrPart.WildStr) - val pname = Identifier.bindableParser.map(StrPart.NamedStr(_)) + val pwild = P.char('_').as(None) + val pname = Identifier.bindableParser.map(Some(_)) + val part: P[Option[Bindable]] = pwild | pname def strp(q: Char): P[List[StrPart]] = - StringUtil.interpolatedString(q, start, pwild.orElse(pname), end) + StringUtil.interpolatedString(q, start, part, end) .map(_.map { case Left(p) => p case Right((_, str)) => StrPart.LitStr(str) diff --git a/core/src/main/scala/org/bykn/bosatsu/Predef.scala b/core/src/main/scala/org/bykn/bosatsu/Predef.scala index 49ad05b35..48a303234 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Predef.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Predef.scala @@ -37,6 +37,7 @@ object Predef { .add(packageName, "trace", FfiCall.Fn2(PredefImpl.trace(_, _))) .add(packageName, "string_Order_fn", FfiCall.Fn2(PredefImpl.string_Order_Fn(_, _))) .add(packageName, "concat_String", FfiCall.Fn1(PredefImpl.concat_String(_))) + .add(packageName, "char_to_String", FfiCall.Fn1(PredefImpl.char_to_String(_))) .add(packageName, "partition_String", FfiCall.Fn2(PredefImpl.partitionString(_, _))) .add(packageName, "rpartition_String", FfiCall.Fn2(PredefImpl.rightPartitionString(_, _))) } @@ -154,6 +155,9 @@ object PredefImpl { case other => sys.error(s"type error: $other") } + // we represent chars as single code-point strings + def char_to_String(item: Value): Value = item + def concat_String(items: Value): Value = items match { case Value.VList(parts) => diff --git a/core/src/main/scala/org/bykn/bosatsu/SourceConverter.scala b/core/src/main/scala/org/bykn/bosatsu/SourceConverter.scala index 5cb247c7f..8f4d1c7b4 100644 --- a/core/src/main/scala/org/bykn/bosatsu/SourceConverter.scala +++ b/core/src/main/scala/org/bykn/bosatsu/SourceConverter.scala @@ -311,25 +311,37 @@ final class SourceConverter( // a single string item should be converted // to that thing, // two or more should be converted this to concat_String([items]) - val decls = parts.map { - case Right((r, str)) => Literal(Lit(str))(r) - case Left(decl) => decl + + def charToString(expr: Expr[Declaration]): Expr[Declaration] = { + val fnName: Expr[Declaration] = + Expr.Global(PackageName.PredefName, Identifier.Name("char_to_String"), expr.tag) + + Expr.buildApp(fnName, expr :: Nil, expr.tag) } - decls match { - case NonEmptyList(one, Nil) => - loop(one) - case twoOrMore => - val lldecl = - ListDecl(ListLang.Cons(twoOrMore.toList.map(SpliceOrItem.Item(_))))(s.region) + val decls = parts.parTraverse { + case StringDecl.Literal(r, str) => loop(Literal(Lit(str))(r)) + case StringDecl.CharExpr(decl) => loop(decl).map(charToString) + case StringDecl.StrExpr(decl) => loop(decl) + } - loop(lldecl).map { listExpr => + decls.map { + case NonEmptyList(one, Nil) => one + case twoOrMore => + def listOf(expr: List[Expr[Declaration]], restDecl: Declaration): Expr[Declaration] = + expr match { + case Nil => + Expr.Global(PackageName.PredefName, Identifier.Constructor("EmptyList"), restDecl) + case h :: tail => + val cons = Expr.Global(PackageName.PredefName, Identifier.Constructor("NonEmptyList"), restDecl) + val tailExpr = listOf(tail, h.tag) + Expr.buildApp(cons, h :: tailExpr :: Nil, restDecl) + } - val fnName: Expr[Declaration] = - Expr.Global(PackageName.PredefName, Identifier.Name("concat_String"), s) + val fnName: Expr[Declaration] = + Expr.Global(PackageName.PredefName, Identifier.Name("concat_String"), s) - Expr.buildApp(fnName, listExpr.replaceTag(s: Declaration) :: Nil, s) - } + Expr.buildApp(fnName, listOf(twoOrMore.toList, s) :: Nil, s) } case l@ListDecl(list) => list match { diff --git a/core/src/main/scala/org/bykn/bosatsu/StringUtil.scala b/core/src/main/scala/org/bykn/bosatsu/StringUtil.scala index 04386f529..ce34f577a 100644 --- a/core/src/main/scala/org/bykn/bosatsu/StringUtil.scala +++ b/core/src/main/scala/org/bykn/bosatsu/StringUtil.scala @@ -1,6 +1,6 @@ package org.bykn.bosatsu -import cats.parse.{Parser0 => P0, Parser => P} +import cats.parse.{Parser0 => P0, Parser => P, Accumulator, Appender} abstract class GenericStringUtil { protected def decodeTable: Map[Char, Char] @@ -14,11 +14,11 @@ abstract class GenericStringUtil { s"\\u$strPad$strHex" }.toArray - val escapedToken: P[Char] = { - def parseIntStr(p: P[Any], base: Int): P[Char] = - p.string.map(Integer.parseInt(_, base).toChar) + val escapedToken: P[Int] = { + def parseIntStr(p: P[Any], base: Int): P[Int] = + p.string.map(java.lang.Integer.parseInt(_, base)) - val escapes = P.charIn(decodeTable.keys.toSeq).map(decodeTable(_)) + val escapes = P.charIn(decodeTable.keys.toSeq).map(decodeTable(_).toInt) val oct = P.charIn('0' to '7') val octP = P.char('o') *> parseIntStr(oct ~ oct, 8) @@ -37,25 +37,70 @@ abstract class GenericStringUtil { P.char('\\') *> after } + val utf16Codepoint: P[Int] = { + // see: https://en.wikipedia.org/wiki/UTF-16 + val first = P.anyChar.map { c => + val ci = c.toInt + if (ci < 0xd800 || ci >= 0xe000) Right(ci) + else Left(ci) + } + + val second: P[Int => Int] = + P.charWhere { c => + val ci = c.toInt + (0xdc00 <= ci) && (ci <= 0xdfff) + } + .map { low => + val lowOff = low - 0xdc00 + 0x10000 + + { high => + val highPart = (high - 0xd800) * 0x400 + highPart + lowOff + } + } + + P.select(first)(second) + } + + val codePointAccumulator: Accumulator[Int, String] = + new Accumulator[Int, String] { + def newAppender(first: Int): Appender[Int,String] = + new Appender[Int, String] { + val strbuilder = new java.lang.StringBuilder + strbuilder.appendCodePoint(first) + + def append(item: Int) = { + strbuilder.appendCodePoint(item) + this + } + def finish(): String = strbuilder.toString + } + } /** * String content without the delimiter */ - def undelimitedString1(endP: P[Unit]): P[String] = - escapedToken.orElse((!endP).with1 *> P.anyChar) - .repAs + def undelimitedString1(endP: P[Unit]): P[String] = { + escapedToken.orElse((!endP).with1 *> utf16Codepoint) + .repAs(codePointAccumulator) + } + + def codepoint(startP: P[Any], endP: P[Any]): P[Int] = + startP *> + escapedToken.orElse((!endP).with1 *> utf16Codepoint) <* + endP def escapedString(q: Char): P[String] = { val end: P[Unit] = P.char(q) end *> undelimitedString1(end).orElse(P.pure("")) <* end } - def interpolatedString[A](quoteChar: Char, istart: P[Unit], interp: P0[A], iend: P[Unit]): P[List[Either[A, (Region, String)]]] = { + def interpolatedString[A, B](quoteChar: Char, istart: P[A => B], interp: P0[A], iend: P[Unit]): P[List[Either[B, (Region, String)]]] = { val strQuote = P.char(quoteChar) - val strLit: P[String] = undelimitedString1(strQuote.orElse(istart)) - val notStr: P[A] = (istart ~ interp ~ iend).map { case ((_, a), _) => a } + val strLit: P[String] = undelimitedString1(strQuote.orElse(istart.void)) + val notStr: P[B] = (istart ~ interp ~ iend).map { case ((fn, a), _) => fn(a) } - val either: P[Either[A, (Region, String)]] = + val either: P[Either[B, (Region, String)]] = ((P.index.with1 ~ strLit ~ P.index).map { case ((s, str), l) => Right((Region(s, l), str)) }) .orElse(notStr.map(Left(_))) diff --git a/core/src/main/scala/org/bykn/bosatsu/TotalityCheck.scala b/core/src/main/scala/org/bykn/bosatsu/TotalityCheck.scala index 41d1c3d27..a5a1bb3c4 100644 --- a/core/src/main/scala/org/bykn/bosatsu/TotalityCheck.scala +++ b/core/src/main/scala/org/bykn/bosatsu/TotalityCheck.scala @@ -86,7 +86,17 @@ case class TotalityCheck(inEnv: TypeEnv[Any]) { case sp@StrPat(_) => val simp = sp.toSeqPattern - if (simp.normalize == simp) validUnit + def hasAdjacentWild[A](seq: SeqPattern[A]): Boolean = + seq match { + case SeqPattern.Empty => false + case SeqPattern.Cat(SeqPart.Wildcard, tail) => + tail match { + case SeqPattern.Cat(SeqPart.Wildcard, _) => true + case notStartWild => hasAdjacentWild(notStartWild) + } + case SeqPattern.Cat(_, tail) => hasAdjacentWild(tail) + } + if (!hasAdjacentWild(simp)) validUnit else Left(NonEmptyList(InvalidStrPat(sp, inEnv), Nil)) case PositionalStruct(name, args) => @@ -315,6 +325,8 @@ case class TotalityCheck(inEnv: TypeEnv[Any]) { case (WildCard, right@StrPat(_)) => // _ is the same as "${_}" for well typed expressions strPatternSetOps.difference(StrPat(NonEmptyList(StrPart.WildStr, Nil)), right) + case (WildCard, Literal(Lit.Str(str))) => + difference(WildCard, StrPat.fromLitStr(str)) case (Var(v), right@StrPat(_)) => // v is the same as "${v}" for well typed expressions strPatternSetOps.difference(StrPat(NonEmptyList(StrPart.NamedStr(v), Nil)), right) diff --git a/core/src/main/scala/org/bykn/bosatsu/TypedExprNormalization.scala b/core/src/main/scala/org/bykn/bosatsu/TypedExprNormalization.scala index 8af0d6ef8..36f1d7bb5 100644 --- a/core/src/main/scala/org/bykn/bosatsu/TypedExprNormalization.scala +++ b/core/src/main/scala/org/bykn/bosatsu/TypedExprNormalization.scala @@ -717,7 +717,8 @@ object TypedExprNormalization { } } } - case EvalResult.Constant(Lit.Str(_)) => + case EvalResult.Constant(Lit.Str(_) | Lit.Chr(_)) => + // TODO, we can match some of these statically None } } diff --git a/core/src/main/scala/org/bykn/bosatsu/Value.scala b/core/src/main/scala/org/bykn/bosatsu/Value.scala index d8948bb8f..2e315e57d 100644 --- a/core/src/main/scala/org/bykn/bosatsu/Value.scala +++ b/core/src/main/scala/org/bykn/bosatsu/Value.scala @@ -187,6 +187,7 @@ object Value { l match { case Lit.Str(s) => ExternalValue(s) case Lit.Integer(i) => ExternalValue(i) + case c @ Lit.Chr(_) => ExternalValue(c.asStr) } object VInt { diff --git a/core/src/main/scala/org/bykn/bosatsu/codegen/python/Code.scala b/core/src/main/scala/org/bykn/bosatsu/codegen/python/Code.scala index 9bac78718..91765f49b 100644 --- a/core/src/main/scala/org/bykn/bosatsu/codegen/python/Code.scala +++ b/core/src/main/scala/org/bykn/bosatsu/codegen/python/Code.scala @@ -4,6 +4,7 @@ import cats.data.NonEmptyList import java.math.BigInteger import org.bykn.bosatsu.{Lit, PredefImpl, StringUtil} import org.typelevel.paiges.Doc +import scala.language.implicitConversions // Structs are represented as tuples // Enums are represented as tuples with an additional first field holding @@ -38,14 +39,31 @@ object Code { def eval(op: Operator, x: Expression): Expression = Op(this, op, x).simplify + def :>(that: Expression): Expression = + Code.Op(this, Code.Const.Gt, that) + + def :<(that: Expression): Expression = + Code.Op(this, Code.Const.Lt, that) + + def =:=(that: Expression): Expression = + Code.Op(this, Code.Const.Eq, that) + def evalAnd(that: Expression): Expression = eval(Const.And, that) def evalPlus(that: Expression): Expression = eval(Const.Plus, that) + def +(that: Expression): Expression = + evalPlus(that) + + def unary_! : Expression = + Code.Ident("not")(this) + def evalMinus(that: Expression): Expression = eval(Const.Minus, that) + + def -(that: Expression): Expression = evalMinus(that) def evalTimes(that: Expression): Expression = eval(Const.Times, that) @@ -53,6 +71,9 @@ object Code { def :=(vl: ValueLike): Statement = addAssign(this, vl) + def len(): Expression = + dot(Code.Ident("__len__"))() + def simplify: Expression } @@ -127,7 +148,7 @@ object Code { case Parens(inner@Parens(_)) => exprToDoc(inner) case Parens(p) => par(exprToDoc(p)) case SelectItem(x, i) => - maybePar(x) + Doc.char('[') + Doc.str(i) + Doc.char(']') + maybePar(x) + Doc.char('[') + exprToDoc(i) + Doc.char(']') case SelectRange(x, os, oe) => val middle = os.fold(Doc.empty)(exprToDoc) + Doc.char(':') + oe.fold(Doc.empty)(exprToDoc) maybePar(x) + (Doc.char('[') + middle + Doc.char(']')).nested(4) @@ -407,17 +428,21 @@ object Code { case exprS => Parens(exprS) } } - case class SelectItem(arg: Expression, position: Int) extends Expression { + case class SelectItem(arg: Expression, position: Expression) extends Expression { def simplify: Expression = - arg.simplify match { - case MakeTuple(items) if items.lengthCompare(position) > 0 => - items(position) - case MakeList(items) if items.lengthCompare(position) > 0 => - items(position) - case simp => - SelectItem(simp, position) + (arg.simplify, position.simplify) match { + case (MakeTuple(items), PyInt(bi)) if items.lengthCompare(bi.intValue()) > 0 => + items(bi.intValue()) + case (MakeList(items), PyInt(bi)) if items.lengthCompare(bi.intValue()) > 0 => + items(bi.intValue()) + case (simp, spos) => + SelectItem(simp, spos) } } + object SelectItem { + def apply(arg: Expression, position: Int): SelectItem = + SelectItem(arg, Code.fromInt(position)) + } // foo[a:b] case class SelectRange(arg: Expression, start: Option[Expression], end: Option[Expression]) extends Expression { def simplify = SelectRange(arg, start.map(_.simplify), end.map(_.simplify)) @@ -508,6 +533,11 @@ object Code { last } } + def if1(cond: Expression, stmt: Statement): Statement = + ifStatement(NonEmptyList.one((cond, stmt)), None) + + def ifElseS(cond: Expression, ifCase: Statement, elseCase: Statement): Statement = + ifStatement(NonEmptyList.one((cond, ifCase)), Some(elseCase)) /* * if __name__ == "__main__": @@ -610,18 +640,22 @@ object Code { lit match { case Lit.Str(s) => PyString(s) case Lit.Integer(bi) => PyInt(bi) + case Lit.Chr(s) => PyString(s) } - def fromInt(i: Int): Expression = + implicit def fromInt(i: Int): Expression = fromLong(i.toLong) - def fromLong(i: Long): Expression = + implicit def fromString(str: String): Expression = + PyString(str) + + implicit def fromLong(i: Long): Expression = if (i == 0L) Const.Zero else if (i == 1L) Const.One else PyInt(BigInteger.valueOf(i)) - def fromBoolean(b: Boolean): Expression = + implicit def fromBoolean(b: Boolean): Expression = if (b) Code.Const.True else Code.Const.False sealed abstract class Operator(val name: String) { diff --git a/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonGen.scala b/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonGen.scala index c02aedc93..0f3a9390a 100644 --- a/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonGen.scala +++ b/core/src/main/scala/org/bykn/bosatsu/codegen/python/PythonGen.scala @@ -275,7 +275,7 @@ object PythonGen { def ifElseS(cond: ValueLike, thenS: Statement, elseS: Statement): Env[Statement] = cond match { - case x: Expression => Monad[Env].pure(ifStatement(NonEmptyList.one((x, thenS)), Some(elseS))) + case x: Expression => Monad[Env].pure(Code.ifElseS(x, thenS, elseS)) case WithValue(stmt, vl) => ifElseS(vl, thenS, elseS).map(stmt +: _) case v => @@ -285,7 +285,7 @@ object PythonGen { .map { tmp => Code.block( tmp := v, - ifStatement(NonEmptyList.one((tmp, thenS)), Some(elseS)) + Code.ifElseS(tmp, thenS, elseS) ) } } @@ -403,10 +403,10 @@ object PythonGen { cont <- Env.newAssignableVar ac = assignMut(cont)(fnArgs.toList) res <- Env.newAssignableVar - ar = Assign(res, Code.Const.Unit) + ar = (res := Code.Const.Unit) body1 <- replaceTailCallWithAssign(selfName, mutArgs.length, body)(assignMut(cont)) setRes = res := body1 - loop = While(cont, Assign(cont, Const.False) +: setRes) + loop = While(cont, (cont := false) +: setRes) newBody = (ac +: ar +: loop).withValue(res) } yield makeDef(selfName, fnArgs, newBody) } @@ -591,7 +591,7 @@ object PythonGen { val selfName = Code.Ident("self") val isAssertion: Code.Expression = - Code.Op(argName.get(0), Code.Const.Eq, Code.fromInt(0)) + argName.get(0) =:= 0 // Assertion(bool, msg) val testAssertion: Code.Statement = @@ -763,16 +763,11 @@ object PythonGen { private val cmpFn: List[ValueLike] => Env[ValueLike] = { input => Env.onLast2(input.head, input.tail.head) { (arg0, arg1) => - // 0 if arg0 < arg1 else ( - // 1 if arg0 == arg1 else 2 - // ) Code.Ternary( - Code.fromInt(0), - Code.Op(arg0, Code.Const.Lt, arg1), - Code.Ternary( - Code.fromInt(1), - Code.Op(arg0, Code.Const.Eq, arg1), - Code.fromInt(2))).simplify + 0, + arg0 :< arg1, + Code.Ternary(1, arg0 =:= arg1, 2) + ).simplify } } @@ -796,7 +791,7 @@ object PythonGen { Code.Ternary( Code.Op(a, Code.Const.Div, b), b, // 0 is false in python - Code.fromInt(0) + 0 ).simplify } }, 2)), @@ -869,7 +864,7 @@ object PythonGen { Env.onLasts(input) { case i :: a :: fn :: Nil => Code.block( - cont := Code.Op(Code.fromInt(0), Code.Const.Lt, i), + cont := (Code.fromInt(0) :< i), res := a, _i := i, _a := a, @@ -878,12 +873,10 @@ object PythonGen { res := fn(_i, _a), tmp_i := res.get(0), _a := res.get(1).get(0), - cont := Code.Op(Code.Op(Code.fromInt(0), Code.Const.Lt, tmp_i), - Code.Const.And, - Code.Op(tmp_i, Code.Const.Lt, _i)), + cont := (Code.fromInt(0) :< tmp_i).evalAnd(tmp_i :< _i), _i := tmp_i - ) - ) + ) + ) ) .withValue(_a) case other => @@ -919,6 +912,9 @@ object PythonGen { case i => Code.Apply(Code.DotSelect(i, Code.Ident("__str__")), Nil) } }, 1)), + (Identifier.unsafeBindable("char_to_String"), + // we encode chars as strings so this is just identity + ({ input => Env.envMonad.pure(input.head) }, 1)), (Identifier.unsafeBindable("trace"), ({ input => Env.onLast2(input.head, input.tail.head) { (msg, i) => @@ -1079,18 +1075,14 @@ object PythonGen { ix match { case EqualsLit(expr, lit) => val literal = Code.litToExpr(lit) - loop(expr).flatMap(Env.onLast(_) { ex => Code.Op(ex, Code.Const.Eq, literal) }) + loop(expr).flatMap(Env.onLast(_) { ex => ex =:= literal }) case EqualsNat(nat, zeroOrSucc) => val natF = loop(nat) if (zeroOrSucc.isZero) - natF.flatMap(Env.onLast(_) { x => - Code.Op(x, Code.Const.Eq, Code.Const.Zero) - }) + natF.flatMap(Env.onLast(_)(_ =:= 0)) else - natF.flatMap(Env.onLast(_) { x => - Code.Op(x, Code.Const.Gt, Code.Const.Zero) - }) + natF.flatMap(Env.onLast(_)(_ :> 0)) case TrueConst => Monad[Env].pure(Code.Const.True) case And(ix1, ix2) => @@ -1103,15 +1095,14 @@ object PythonGen { // otherwise, we use tuples with the first // item being the variant val useInts = famArities.forall(_ == 0) - val idxExpr = Code.fromInt(idx) loop(enumV).flatMap { tup => Env.onLast(tup) { t => if (useInts) { // this is represented as an integer - Code.Op(t, Code.Const.Eq, idxExpr) + t =:= idx } else - Code.Op(t.get(0), Code.Const.Eq, idxExpr) + t.get(0) =:= idx } } case SetMut(LocalAnonMut(mut), expr) => @@ -1139,7 +1130,7 @@ object PythonGen { } def matchString(strEx: Expression, pat: List[StrPart], binds: List[Code.Ident]): Env[ValueLike] = { - import StrPart.{LitStr, Glob} + import StrPart.{LitStr, Glob, CharPart} val bindArray = binds.toArray // return a value like expression that contains the boolean result // and assigns all the bindings along the way @@ -1147,7 +1138,7 @@ object PythonGen { pat match { case Nil => //offset == str.length - Monad[Env].pure(Code.Op(offsetIdent, Code.Const.Eq, strEx.dot(Code.Ident("__len__"))())) + Monad[Env].pure(offsetIdent =:= strEx.len()) case LitStr(expect) :: tail => //val len = expect.length //str.regionMatches(offset, expect, 0, len) && loop(offset + len, tail, next) @@ -1155,14 +1146,32 @@ object PythonGen { // strEx.startswith(expect, offsetIdent) loop(offsetIdent, tail, next) .flatMap { loopRes => - val regionMatches = strEx.dot(Code.Ident("startswith"))(Code.PyString(expect), offsetIdent) + val regionMatches = strEx.dot(Code.Ident("startswith"))(expect, offsetIdent) val rest = ( - offsetIdent := (offsetIdent.evalPlus(Code.fromInt(expect.length))) + offsetIdent := offsetIdent + expect.length ).withValue(loopRes) Env.andCode(regionMatches, rest) } + case (c: CharPart) :: tail => + val matches = offsetIdent :< strEx.len() + val n1 = if (c.capture) (next + 1) else next + val stmt = + if (c.capture) { + // b = str[offset] + Code.block( + bindArray(next) := Code.SelectItem(strEx, offsetIdent), + offsetIdent := offsetIdent + 1 + ) + .withValue(true) + } + else (offsetIdent := offsetIdent + 1).withValue(true) + for { + tailRes <- loop(offsetIdent, tail, n1) + and2 <- Env.andCode(stmt, tailRes) + and1 <- Env.andCode(matches, and2) + } yield and1 case (h: Glob) :: tail => tail match { case Nil => @@ -1171,7 +1180,7 @@ object PythonGen { if (h.capture) { // b = str[offset:] (bindArray(next) := Code.SelectRange(strEx, Some(offsetIdent), None)) - .withValue(Code.Const.True) + .withValue(true) } else Code.Const.True ) @@ -1225,31 +1234,28 @@ object PythonGen { val capture = if (h.capture) (bindArray(next) := Code.SelectRange(strEx, Some(offsetIdent), Some(candidate))) else Code.Pass Code.block( capture, - result := Code.Const.True, - start := Code.fromInt(-1) + result := true, + start := -1 ) }, { // we couldn't match at start, advance just after the // candidate - start := candidate.evalPlus(Code.fromInt(1)) + start := candidate + 1 }) def findBranch(search: ValueLike): Env[Statement] = onSearch(search) .flatMap { onS => Env.ifElseS( - Code.Op(candidate, Code.Const.Gt, Code.fromInt(-1)), - { - // update candidate and search - Code.block( - candOffset := Code.Op(candidate, Code.Const.Plus, Code.fromInt(expect.length)), - onS) - }, - { - // else no more candidates - start := Code.fromInt(-1) - } + candidate :> -1, + // update candidate and search + Code.block( + candOffset := candidate + expect.length, + onS) + , + // else no more candidates + start := -1 ) } @@ -1259,10 +1265,10 @@ object PythonGen { } yield (Code.block( start := offsetIdent, - result := Code.Const.False, - Code.While(Code.Op(start, Code.Const.Gt, Code.fromInt(-1)), + result := false, + Code.While((start :> -1), Code.block( - candidate := strEx.dot(Code.Ident("find"))(Code.PyString(expect), start), + candidate := strEx.dot(Code.Ident("find"))(expect, start), find ) ) @@ -1270,18 +1276,41 @@ object PythonGen { .withValue(result)) } .flatten + case (_: CharPart) :: _ => + val next1 = if (h.capture) (next + 1) else next + for { + matched <- Env.newAssignableVar + off1 <- Env.newAssignableVar + tailMatched <- loop(off1, tail, next1) + + matchStmt = Code.block( + matched := false, + off1 := offsetIdent, + Code.While((!matched).evalAnd(off1 :< strEx.len()), + matched := tailMatched // the tail match increments the + ) + ).withValue(matched) + + capture = Code.block( + bindArray(next) := Code.SelectRange(strEx, Some(offsetIdent), Some(off1)) + ).withValue(true) + + fullMatch <- + if (!h.capture) Monad[Env].pure(matchStmt) + else Env.andCode(matchStmt, capture) + + } yield fullMatch + // $COVERAGE-OFF$ case (_: Glob) :: _ => - // $COVERAGE-OFF$ throw new IllegalArgumentException(s"pattern: $pat should have been prevented: adjacent globs are not permitted (one is always empty)") - // $COVERAGE-ON$ + // $COVERAGE-ON$ } } - Env.newAssignableVar - .flatMap { offsetIdent => - loop(offsetIdent, pat, 0) - .map { res => (offsetIdent := Code.fromInt(0)).withValue(res) } - } + for { + offsetIdent <- Env.newAssignableVar + res <- loop(offsetIdent, pat, 0) + } yield (offsetIdent := 0).withValue(res) } def searchList(locMut: LocalAnonMut, initVL: ValueLike, checkVL: ValueLike, optLeft: Option[LocalAnonMut]): Env[ValueLike] = { @@ -1323,11 +1352,10 @@ object PythonGen { Code.block( currentList := tmpList, res := checkVL, - Code.ifStatement( - NonEmptyList( - (res, (tmpList := emptyList)), - Nil), - Some { + Code.ifElseS( + res, + tmpList := emptyList, + { Code.block( tmpList := tailList(tmpList), optLeft.fold(Code.pass) { left => diff --git a/core/src/main/scala/org/bykn/bosatsu/pattern/SeqPattern.scala b/core/src/main/scala/org/bykn/bosatsu/pattern/SeqPattern.scala index d9a827b3f..f67f20219 100644 --- a/core/src/main/scala/org/bykn/bosatsu/pattern/SeqPattern.scala +++ b/core/src/main/scala/org/bykn/bosatsu/pattern/SeqPattern.scala @@ -105,10 +105,8 @@ object SeqPattern { } def fromList[A](ps: List[SeqPart[A]]): SeqPattern[A] = - ps match { - case h :: tail => - Cat(h, fromList(tail)) - case Nil => Empty + ps.foldRight(Empty: SeqPattern[A]) { (h, tail) => + Cat(h, tail) } val Wild: SeqPattern[Nothing] = Cat(SeqPart.Wildcard, Empty) diff --git a/core/src/main/scala/org/bykn/bosatsu/rankn/Infer.scala b/core/src/main/scala/org/bykn/bosatsu/rankn/Infer.scala index 855abe6af..81d16ae88 100644 --- a/core/src/main/scala/org/bykn/bosatsu/rankn/Infer.scala +++ b/core/src/main/scala/org/bykn/bosatsu/rankn/Infer.scala @@ -1077,6 +1077,7 @@ object Infer { } val names = items.collect { case GenPattern.StrPart.NamedStr(n) => (n, tpe) + case GenPattern.StrPart.NamedChar(n) => (n, Type.CharType) } // we need to apply the type so the names are well typed val anpat = GenPattern.Annotation(pat, tpe) diff --git a/core/src/main/scala/org/bykn/bosatsu/rankn/Type.scala b/core/src/main/scala/org/bykn/bosatsu/rankn/Type.scala index 0677a41b5..85c7a4da3 100644 --- a/core/src/main/scala/org/bykn/bosatsu/rankn/Type.scala +++ b/core/src/main/scala/org/bykn/bosatsu/rankn/Type.scala @@ -124,6 +124,7 @@ object Type { lit match { case Lit.Integer(_) => Type.IntType case Lit.Str(_) => Type.StrType + case Lit.Chr(_) => Type.CharType } /** @@ -370,6 +371,7 @@ object Type { val ListType: Type.TyConst = TyConst(Const.predef("List")) val OptionType: Type.TyConst = TyConst(Const.predef("Option")) val StrType: Type.TyConst = TyConst(Const.predef("String")) + val CharType: Type.TyConst = TyConst(Const.predef("Char")) val TestType: Type.TyConst = TyConst(Const.predef("Test")) val TupleConsType: Type.TyConst = TyConst(Type.Const.predef("TupleCons")) val UnitType: Type.TyConst = TyConst(Type.Const.predef("Unit")) @@ -381,6 +383,7 @@ object Type { IntType -> Kind.Type, ListType -> Kind(Kind.Type.co), StrType -> Kind.Type, + CharType -> Kind.Type, UnitType -> Kind.Type, TupleConsType -> Kind(Kind.Type.co, Kind.Type.co), )) diff --git a/core/src/test/scala/org/bykn/bosatsu/EvaluationTest.scala b/core/src/test/scala/org/bykn/bosatsu/EvaluationTest.scala index 1eadac35c..850352c11 100644 --- a/core/src/test/scala/org/bykn/bosatsu/EvaluationTest.scala +++ b/core/src/test/scala/org/bykn/bosatsu/EvaluationTest.scala @@ -2349,7 +2349,7 @@ main = match x: """)) { case sce@PackageError.TotalityCheckError(_, _) => val dollar = '$' assert(sce.message(Map.empty, Colorize.None) == - s"in file: , package Err\nRegion(36,91)\ninvalid string pattern: '$dollar{_}$dollar{_}' (adjacent bindings aren't allowed)") + s"in file: , package Err\nRegion(36,91)\ninvalid string pattern: '$dollar{_}$dollar{_}' (adjacent string bindings aren't allowed)") () } } @@ -3123,4 +3123,33 @@ z = ( () } } + + test("test character literals") { + runBosatsuTest( + List(""" +package Foo + +good1 = match .'x': + case .'y': False + case .'x': True + case _: False + +test1 = Assertion(good1, "simple match") + +just_x = .'x' +good2 = match "$.{just_x}": + case "$.{x}": x matches .'x' + case _: False + +test2 = Assertion(good2, "interpolation match") + +def last(str) -> Option[Char]: + match str: + case "": None + case "${_}$.{c}": Some(c) + +test3 = Assertion(last("foo") matches Some(.'o'), "last test") +all = TestSuite("chars", [test1, test2, test3]) +"""), "Foo", 3) + } } diff --git a/core/src/test/scala/org/bykn/bosatsu/Gen.scala b/core/src/test/scala/org/bykn/bosatsu/Gen.scala index 4e06d71d5..d4bd64551 100644 --- a/core/src/test/scala/org/bykn/bosatsu/Gen.scala +++ b/core/src/test/scala/org/bykn/bosatsu/Gen.scala @@ -169,10 +169,14 @@ object Generators { } def genStringDecl(dec0: Gen[NonBinding]): Gen[Declaration.StringDecl] = { + import Declaration.StringDecl + val item = Gen.oneOf( - Arbitrary.arbitrary[String].filter(_.length > 1).map { s => Right((emptyRegion, s)) }, - dec0.map(Left(_))) + Arbitrary.arbitrary[String].filter(_.length > 1).map { s => StringDecl.Literal(emptyRegion, s) }, + dec0.map(StringDecl.StrExpr(_)), + dec0.map(StringDecl.CharExpr(_)), + ) def removeAdj[A](nea: NonEmptyList[A])(fn: (A, A) => Boolean): NonEmptyList[A] = nea match { @@ -186,11 +190,14 @@ object Generators { lst <- Gen.listOfN(sz, item) nel = NonEmptyList.fromListUnsafe(lst) // make sure we don't have two adjacent strings - nel1 = removeAdj(nel) { (a1, a2) => a1.isRight && a2.isRight } + nel1 = removeAdj(nel) { + case (StringDecl.Literal(_, _), StringDecl.Literal(_, _)) => true + case _ => false + } } yield Declaration.StringDecl(nel1)(emptyRegion) res.filter { - case Declaration.StringDecl(NonEmptyList(Right(_), Nil)) => + case Declaration.StringDecl(NonEmptyList(StringDecl.Literal(_, _), Nil)) => false case _ => true } @@ -437,11 +444,15 @@ object Generators { Gen.oneOf( lowerIdent.map(Pattern.StrPart.LitStr(_)), bindIdentGen.map(Pattern.StrPart.NamedStr(_)), - Gen.const(Pattern.StrPart.WildStr)) + bindIdentGen.map(Pattern.StrPart.NamedChar(_)), + Gen.const(Pattern.StrPart.WildStr), + Gen.const(Pattern.StrPart.WildChar)) def isWild(p: Pattern.StrPart): Boolean = p match { - case Pattern.StrPart.LitStr(_) => false + case Pattern.StrPart.LitStr(_) | + Pattern.StrPart.NamedChar(_) | + Pattern.StrPart.WildChar => false case _ => true } @@ -559,8 +570,10 @@ object Generators { str <- lowerIdent // TODO } yield Lit.Str(str) + val char = Gen.choose(0, 0xd7ff).map { i => Lit.Chr.fromCodePoint(i) } + val bi = Arbitrary.arbitrary[BigInt].map { bi => Lit.Integer(bi.bigInteger) } - Gen.oneOf(str, bi) + Gen.oneOf(str, bi, char) } val identifierGen: Gen[Identifier] = @@ -742,8 +755,9 @@ object Generators { case Var(_) => Stream.empty case StringDecl(parts) => parts.toList.toStream.map { - case Left(nb) => nb - case Right((r, str)) => Literal(Lit.Str(str))(r) + case StringDecl.StrExpr(nb) => nb + case StringDecl.CharExpr(nb) => nb + case StringDecl.Literal(r, str) => Literal(Lit.Str(str))(r) } case ListDecl(ListLang.Cons(items)) => items.map(_.value).toStream diff --git a/core/src/test/scala/org/bykn/bosatsu/LitTest.scala b/core/src/test/scala/org/bykn/bosatsu/LitTest.scala new file mode 100644 index 000000000..9ff3239c9 --- /dev/null +++ b/core/src/test/scala/org/bykn/bosatsu/LitTest.scala @@ -0,0 +1,62 @@ +package org.bykn.bosatsu + +import org.scalacheck.Gen +import org.scalatest.funsuite.AnyFunSuite +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks.{ forAll, PropertyCheckConfiguration } +import org.scalacheck.Arbitrary +import org.typelevel.paiges.Document + +class LitTest extends AnyFunSuite { + def config: PropertyCheckConfiguration = + PropertyCheckConfiguration(minSuccessful = if (Platform.isScalaJvm) 1000 else 100) + + val genLit: Gen[Lit] = + Gen.oneOf( + Gen.choose(-10000, 10000).map(Lit.fromInt), + Arbitrary.arbitrary[String].map(Lit(_)), + Gen.frequency( + (10, Gen.choose(0, 0xd800)), + (1, Gen.choose(0xe000, 1000000))).map(Lit.fromCodePoint) + ) + + test("we can convert from Char to Lit") { + forAll(Gen.choose(Char.MinValue, Char.MaxValue)) { (c: Char) => + try { + val chr = Lit.fromChar(c) + assert(chr.asInstanceOf[Lit.Chr].asStr == c.toString) + } + catch { + case _: IllegalArgumentException => + // there are at least 1million valid codepoints + val cp = c.toInt + assert((0xd800 <= cp && cp < 0xe000)) + } + } + } + + test("we can convert from Codepoint to Lit") { + forAll(Gen.choose(-1000, 1500000)) { (cp: Int) => + try { + val chr = Lit.fromCodePoint(cp) + assert(chr.asInstanceOf[Lit.Chr].toCodePoint == cp) + } + catch { + case _: IllegalArgumentException => + // there are at least 1million valid codepoints + assert(cp < 0 || (0xd800 <= cp && cp < 0xe000) || (cp > 1000000)) + } + } + } + + test("Lit ordering is correct") { + forAll(genLit, genLit, genLit) { (a, b, c) => + OrderingLaws.law(a, b, c) + } + } + + test("we can parse from document") { + forAll(genLit) { l => + assert(Lit.parser.parseAll(Document[Lit].document(l).render(80)) == Right(l)) + } + } +} \ No newline at end of file diff --git a/core/src/test/scala/org/bykn/bosatsu/ParserTest.scala b/core/src/test/scala/org/bykn/bosatsu/ParserTest.scala index af4872339..9e680d310 100644 --- a/core/src/test/scala/org/bykn/bosatsu/ParserTest.scala +++ b/core/src/test/scala/org/bykn/bosatsu/ParserTest.scala @@ -213,7 +213,7 @@ class ParserTest extends ParserTestBase { def singleq(str1: String, res: List[Either[Json, String]]) = parseTestAll( StringUtil - .interpolatedString('\'', P.string("${"), Json.parser, P.char('}')) + .interpolatedString('\'', P.string("${").as((j: Json) => j), Json.parser, P.char('}')) .map(_.map { case Right((_, str)) => Right(str) case Left(l) => Left(l) @@ -233,6 +233,35 @@ class ParserTest extends ParserTestBase { singleq(s"'$dollar{42}bar'", List(Left(Json.JNumberStr("42")), Right("bar"))) } + test("we can decode any utf16") { + val p = StringUtil.utf16Codepoint.repAs(StringUtil.codePointAccumulator) | P.pure("") + val genCodePoints: Gen[Int] = + Gen.frequency( + (10, Gen.choose(0, 0xd7ff)), + (1, Gen.choose(0, 0x10ffff).filterNot { cp => + (0xD800 <= cp && cp <= 0xDFFF) + }) + ) + + // .codePoints isn't available in scalajs + def jsCompatCodepoints(s: String): List[Int] = + if (s.isEmpty) Nil + else (s.codePointAt(0) :: jsCompatCodepoints(s.substring(s.offsetByCodePoints(0, 1)))) + + forAll(Gen.listOf(genCodePoints)) { cps => + val strbuilder = new java.lang.StringBuilder + cps.foreach(strbuilder.appendCodePoint(_)) + val str = strbuilder.toString + val hex = cps.map(_.toHexString) + + val parsed = p.parseAll(str) + assert(parsed == Right(str)) + + assert(parsed.map(jsCompatCodepoints) == Right(cps), + s"hex = $hex, str = ${jsCompatCodepoints(str)} utf16 = ${str.toCharArray().toList.map(_.toInt.toHexString)}") + } + } + test("Identifier round trips") { forAll(Generators.identifierGen)(law(Identifier.parser)) @@ -680,6 +709,7 @@ x""") roundTrip(Pattern.matchParser, "_ as foo") roundTrip(Pattern.matchParser, "Some(_) as foo | None") roundTrip(Pattern.matchParser, "Bar | Some(_) as foo | None") + roundTrip(Pattern.matchParser, """"foo${bar}$.{codepoint}"""") roundTrip(Pattern.bindParser, "x: Int") implicit def docList[A: Document]: Document[NonEmptyList[A]] = diff --git a/core/src/test/scala/org/bykn/bosatsu/TotalityTest.scala b/core/src/test/scala/org/bykn/bosatsu/TotalityTest.scala index 55e47bd34..d9ff47c18 100644 --- a/core/src/test/scala/org/bykn/bosatsu/TotalityTest.scala +++ b/core/src/test/scala/org/bykn/bosatsu/TotalityTest.scala @@ -15,8 +15,6 @@ import org.typelevel.paiges.Document import Identifier.Constructor -import cats.implicits._ - class TotalityTest extends SetOpsLaws[Pattern[(PackageName, Constructor), Type]] { type Pat = Pattern[(PackageName, Constructor), Type] @@ -394,4 +392,26 @@ enum Either: Left(l), Right(r) regressions.foreach { case (a, b) => emptyIntersectionMeansDiffIdent(a, b, eqPatterns) } } + + test("difference returns distinct regressions") { + def check(str: String) = { + val List(p1, p2) = patterns(str) + val tc = TotalityCheck(predefTE) + val diff = tc.difference(p1, p2) + assert(diff == diff.distinct) + } + + check("""["${foo}$.{_}", "$.{bar}$.{_}$.{_}"]""") + } + test("string match totality") { + val tc = TotalityCheck(predefTE) + + val ps = patterns("""["${_}$.{_}", ""]""") + val diff = tc.missingBranches(ps) + assert(diff == Nil) + + val ps1 = patterns("""["", "$.{_}${_}"]""") + val diff1 = tc.missingBranches(ps1) + assert(diff1 == Nil) + } } diff --git a/core/src/test/scala/org/bykn/bosatsu/pattern/SeqPatternTest.scala b/core/src/test/scala/org/bykn/bosatsu/pattern/SeqPatternTest.scala index fb78105ed..bb67c8df4 100644 --- a/core/src/test/scala/org/bykn/bosatsu/pattern/SeqPatternTest.scala +++ b/core/src/test/scala/org/bykn/bosatsu/pattern/SeqPatternTest.scala @@ -343,6 +343,12 @@ abstract class SeqPatternLaws[E, I, S, R] extends AnyFunSuite { forAll(genNamed, genSeq)(namedMatchesPatternLaw(_, _)) } + test("* - [] - [_, *] == empty") { + val diff1 = setOps.difference(Cat(Wildcard, Empty), Empty) + assert(diff1.flatMap(setOps.difference(_, Cat(AnyElem, Cat(Wildcard, Empty)))) == Nil) + assert(diff1.flatMap(setOps.difference(_, Cat(Wildcard, Cat(AnyElem, Empty)))) == Nil) + } + /* test("if x - y is empty, (x + z) - (y + z) is empty") { forAll { (x0: Pattern, y0: Pattern, z0: Pattern) => diff --git a/test_workspace/Char.bosatsu b/test_workspace/Char.bosatsu new file mode 100644 index 000000000..6a1be0b31 --- /dev/null +++ b/test_workspace/Char.bosatsu @@ -0,0 +1,52 @@ +package Bosatsu/Char + +def string_to_Char(s: String) -> Option[Char]: + match s: + case "$.{c}": Some(c) + case _: None + +str_to_char_tests = TestSuite("string_to_Char", + [ + Assertion(string_to_Char("s") matches Some(.'s'), "s"), + Assertion(string_to_Char("") matches None, "empty"), + Assertion(string_to_Char("foo") matches None, "foo"), + ] +) + +def length_String(s: String) -> Int: + def loop(s, acc): + recur s: + case "": acc + case "$.{_}${tail}": loop(tail, acc.add(1)) + + loop(s, 0) + +len_test = TestSuite("len tests", + [ + Assertion(length_String("") matches 0, "empty"), + Assertion(length_String("x") matches 1, "x"), + Assertion(length_String("hello") matches 5, "hello"), + ] +) + +def last_String(s: String) -> Option[Char]: + match s: + case "": None + case "${_}$.{l}": Some(l) + +last_tests = TestSuite( + "last_String", + [ + Assertion(last_String("") matches None, "empty"), + Assertion(last_String("x") matches Some(.'x'), "x"), + Assertion(last_String("1234") matches Some(.'4'), "1234"), + ] +) + +tests = TestSuite("Char tests", + [ + str_to_char_tests, + len_test, + last_tests, + ] +) \ No newline at end of file