diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index a3cd035e58..9624e9a5b6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -28,6 +28,7 @@ case class Config( stageCode: Bool, target: CompilationTarget, rewriteWhileLoops: Bool, + classTags: Opt[ClassTags], qqEnabled: Bool, funcToCls: Bool, commentGeneratedCode: Bool, @@ -80,6 +81,7 @@ object Config: target = CompilationTarget.JS, rewriteWhileLoops = false, stageCode = false, + classTags = N, qqEnabled = false, funcToCls = false, commentGeneratedCode = false, @@ -212,6 +214,13 @@ object Config: def withDebug(debug: Bool) = FlowBasedOpt(default.config.copy(debug = debug)) + case class ClassTags(debug: Bool, mono: Bool) + object ClassTags: + val default = ClassTags( + debug = false, + mono = false, + ) + /** `altSmallThreshold` is the alternative threshold for inlining things into @inline functions. * Normally, we avoid inlining into @inline functions as that could lead to unexpected code bloat. */ case class Inliner(inlineThreshold: Int, altSmallThreshold: Int = 2) @@ -595,6 +604,24 @@ object ConfigParser: ).map: Config.FlowBasedOpt.apply + private def parseClassTags(tree: Tree, current: Opt[Config.ClassTags])(using Raise): Opt[Config.ClassTags] = + tree match + case Call("ClassTags", args) => + val base = current.getOrElse(Config.ClassTags.default) + var debug = base.debug + var mono = base.mono + args.foreach: + case NamedArg("debug", value) => + setFrom(value)(parseBool)(v => debug = v) + case NamedArg("mono", value) => + setFrom(value)(parseBool)(v => mono = v) + case other => + unsupported("ClassTags", other) + S(Config.ClassTags(debug, mono)) + case _ => + expect("ClassTags(...)")(tree) + N + /** Parse a single field override like `tailRecOpt: false`. */ private def parseField(name: Str, value: Tree)(using Raise): Config => Config = name match case "language" => parseLanguageOverride(value) @@ -626,6 +653,10 @@ object ConfigParser: optionalFieldWithCurrent(value)(_.deforest)( (tree, current) => parseDeforest(tree, current) )(v => _.mapOptimizer(_.copy(deforest = v))) + case "classTags" => + optionalFieldWithCurrent(value)(_.classTags)( + (tree, current) => parseClassTags(tree, current) + )(v => _.copy(classTags = v)) case "flowBasedOpt" => optionalFieldWithCurrent(value)(_.flowBasedOpt)( (tree, current) => parseFlowBasedOpt(tree, current) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassTagsTransformer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassTagsTransformer.scala new file mode 100644 index 0000000000..0d7b687e75 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassTagsTransformer.scala @@ -0,0 +1,888 @@ +package hkmc2 +package codegen + +import hkmc2.utils.*, shorthands.* +import utils.* +import Message.MessageContext + +import semantics.* +import flowAnalysis.* + +import hkmc2.semantics.Elaborator.State + +import scala.collection.mutable.{Set as MutSet, Map as MutMap} +import scala.collection.mutable.ListBuffer + +type Web = FlowWebComputation.Result[Ctor, ConcreteCtorConsumer] + +private object ClassTagsDebug: + def showCtor(ctor: CtorCls): Str = ctor match + case cls: ClassLikeSymbol => cls.nme + case size: Int => s"tup(size $size)" + + def showField(field: SelField): Str = field match + case sym: TermSymbol => sym.nme + case index: Int => index.toString + + def showProducer(producer: Ctor): Str = + s"${showCtor(producer.ctor)}@${producer.exprId}" + + def showFieldAccess(access: FieldSel): Str = + s"${{showCtor(access.selectsFrom)}}.${showField(access.field)}@${access.exprId}" + + def showPatternMatch(patternMatch: Dtor): Str = + s"match@${patternMatch.exprId}" + + def showConsumer(consumer: ConcreteCtorConsumer): Str = consumer match + case access: FieldSel => showFieldAccess(access) + case patternMatch: Dtor => showPatternMatch(patternMatch) + +// * Collect all producers & consumers in the given function to build the web +class WebEntryCollector(val flowRes: FlowConstraintSolver)(using val tl: TL) extends BlockTraverser: + private given fState: FlowAnalysis.State = flowRes.fState + private given eState: State = flowRes.eState + + private val entryPoints = ListBuffer.empty[WebEntryCollector.EntryPoints] + private val concreteCtorsByResultId = MutMap.empty[ResultId, ListBuffer[Ctor]] + for ctor <- flowRes.ctorsWithDests do + concreteCtorsByResultId.getOrElseUpdate(ctor.exprId, ListBuffer.empty) += ctor + private val concreteConsumersByResultId = MutMap.empty[ResultId, ListBuffer[ConcreteCtorConsumer]] + for consumer <- flowRes.consumersWithSrcs do + concreteConsumersByResultId.getOrElseUpdate(consumer.exprId, ListBuffer.empty) += consumer + + private class ResultCollector extends BlockTraverserShallow: + val resultIds: ListBuffer[ResultId] = ListBuffer.empty + + override def applyResult(r: Result): Unit = + resultIds += r.uid + super.applyResult(r) + end ResultCollector + + override def applyFunDefn(fun: FunDefn): Unit = + val funName = fun.owner.fold(fun.dSym.nme)(owner => s"${owner.nme}.${fun.dSym.nme}") + val collector = new ResultCollector() + collector.applyBlock(fun.body) + + val seenProducerEntryPoints = MutSet.empty[Ctor] + for + resultId <- collector.resultIds + ctor <- concreteCtorsByResultId.getOrElse(resultId, Nil) + if !ctor.dests.contains(UnknownCons) // does not leak out of the web + if !ctor.dests.exists: + case consumer: ConcreteCtorConsumer => consumer.srcs.contains(UnknownProd) + case _ => false + do seenProducerEntryPoints.add(ctor) + + if !seenProducerEntryPoints.isEmpty then + tl.log(s"track construction of ${seenProducerEntryPoints.map(ClassTagsDebug.showProducer).mkString(", ")} in $funName") + + val seenConsumerEntryPoints = MutSet.empty[ConcreteCtorConsumer] + for + resultId <- collector.resultIds + consumer <- concreteConsumersByResultId.getOrElse(resultId, Nil) + if !consumer.srcs.contains(UnknownProd) // not allocated out of the web + if !consumer.srcs.exists: + case ctor: Ctor => ctor.dests.contains(UnknownCons) + case _ => false + if consumer.srcs.exists: + case _: Ctor => true + case _ => false + do seenConsumerEntryPoints.add(consumer) + + if !seenConsumerEntryPoints.isEmpty then + tl.log(s"track consumption at ${seenConsumerEntryPoints.map(ClassTagsDebug.showConsumer).mkString(", ")} in $funName") + + entryPoints += WebEntryCollector.EntryPoints( + seenProducerEntryPoints.toList, + seenConsumerEntryPoints.toList, + ) + + def result: List[WebEntryCollector.EntryPoints] = entryPoints.toList + +object WebEntryCollector: + case class EntryPoints(producers: List[Ctor], consumers: List[ConcreteCtorConsumer]) + + def apply(p: Program, flowRes: FlowConstraintSolver)(using TL): List[EntryPoints] = + val collector = new WebEntryCollector(flowRes) + collector.applyProgram(p) + collector.result + + +private sealed abstract class Shape: + def show: Str + + final def flattenShape(at: Opt[Loc])(using raise: Raise): List[Shape] = + flattenShape(at, Shape.defaultFlattenThreshold) + + final def flattenShape(at: Opt[Loc], threshold: Int)(using raise: Raise): List[Shape] = + val result = flattenShape + if result.length > threshold then + raise(ErrorReport( + msg"Flattening this shape would produce more than $threshold alternatives." -> at :: Nil, + source = Diagnostic.Source.Compilation, + )) + Nil + else result + + def flattenShape: List[Shape] + + def containsUnion: Bool + + // * This shape subsumption is only used for wildcards (dynamic shapes) checking. + // * i.e., if the pattern is a wildcard, it can accept any scrutinee + // * We do not support a union shape for pattern + // * and we flattern unions in Ctor to insert different tags + // * We also track precise information so we do not need to check if a class is a subclass of another. + // * i.e., if a variable has shape C, then it is impossible that the variable is instantiated to a subclass D in runtime. + final infix def <=(that: Shape): Bool = (this, that) match + case (_, DynamicShape) => true + case (LitShape(left), LitShape(right)) => left === right + case (ClassShape(leftCtor, leftFields), ClassShape(rightCtor, rightFields)) => + leftCtor === rightCtor + && leftFields.keySet === rightFields.keySet + && leftFields.forall: (field, shape) => + shape <= rightFields(field) + case (TupleShape(leftLength, leftElements), TupleShape(rightLength, rightElements)) => + leftLength === rightLength + && leftElements.zip(rightElements).forall((left, right) => left <= right) + case _ => false + +private object Shape: + val defaultFlattenThreshold: Int = 256 + + def mkShapeByPattern(pattern: Pattern)(using raise: Raise): Shape = + pattern match + case ctorPattern @ Pattern.Constructor(_, arguments) => + val ctor = ctorPattern.symbol.flatMap: + case ctor: ClassCtorSymbol => S(ctor.associatedCls) + case symbol => symbol.asClsLike + ctor match + case S(cls: ClassSymbol) => + cls.tree.clsParams match + case fields :: Nil => + val argumentShapes = arguments match + case S(patterns) => patterns.map(mkShapeByPattern) + case N => Nil + if argumentShapes.size =/= fields.size then + raise(ErrorReport( + msg"Expected constructor arity ${fields.size} in @matchShapes pattern for ${cls.nme}, but found ${argumentShapes.size}." -> pattern.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + DynamicShape + else ClassShape(cls, fields.zip(argumentShapes).toMap) + case _ => + raise(ErrorReport( + msg"This pattern is not supported by @matchShapes yet." -> pattern.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + DynamicShape + case S(obj: ModuleOrObjectSymbol) => + ClassShape(obj, Map.empty) + case _ => DynamicShape + case Pattern.Tuple(leading, N) => + TupleShape(leading.size, leading.map(mkShapeByPattern)) + case Pattern.Literal(literal) => LitShape(Value.Lit(literal)(literal.toLoc)) + case Pattern.Wildcard() => DynamicShape + case _ => + raise(ErrorReport( + msg"This pattern is not supported by @matchShapes yet." -> pattern.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + DynamicShape + +private case class LitShape(lit: Value.Lit) extends Shape: + def show: Str = lit match + case Value.Lit(lit) => lit.idStr + + def flattenShape: List[Shape] = this :: Nil + + def containsUnion: Bool = false + +private case class ClassShape(ctor: ClassLikeSymbol, fields: Map[TermSymbol, Shape]) extends Shape: + def show: Str = + if fields.isEmpty then ClassTagsDebug.showCtor(ctor) + else + val shownFields = fields.iterator + .map((field, shape) => s"${ClassTagsDebug.showField(field)}: ${shape.show}") + s"${ClassTagsDebug.showCtor(ctor)}${shownFields.mkString("(", ", ", ")")}" + + def flattenShape: List[Shape] = + val alternatives = fields.iterator.foldLeft(List(Map.empty[TermSymbol, Shape])): + case (alternatives, (field, fieldShape)) => + for + alternative <- alternatives + concreteFieldShape <- fieldShape.flattenShape + yield alternative.updated(field, concreteFieldShape) + alternatives.map(ClassShape(ctor, _)).distinct + + def containsUnion: Bool = fields.valuesIterator.exists(_.containsUnion) + +private case class TupleShape(length: Int, elements: List[Shape]) extends Shape: + require(elements.length === length) + def show: Str = + if elements.isEmpty then ClassTagsDebug.showCtor(length) + else s"${ClassTagsDebug.showCtor(length)}${elements.map(_.show).mkString("(", ", ", ")")}" + + def flattenShape: List[Shape] = + val alternatives = elements.foldLeft(List(List.empty[Shape])): + case (alternatives, element) => + for + alternative <- alternatives + concreteElement <- element.flattenShape + yield alternative :+ concreteElement + alternatives.map(TupleShape(length, _)).distinct + + def containsUnion: Bool = elements.exists(_.containsUnion) + +private case class UnionShape(subshapes: List[Shape]) extends Shape: + def show: Str = subshapes.map(_.show).mkString("(", " | ", ")") + + def flattenShape: List[Shape] = + subshapes.flatMap(_.flattenShape).distinct + + def containsUnion: Bool = true + +private object UnionShape: + def mkUnion(shapes: Iterable[Shape]): Shape = + val flattened = shapes.iterator.flatMap: + case UnionShape(subshapes) if subshapes.nonEmpty => subshapes + case shape => shape :: Nil + val normalized = flattened.toList.distinct.sortBy(_.show) + normalized match + case Nil => DynamicShape + case shape :: Nil => shape + case shapes => UnionShape(shapes) + +private object DynamicShape extends Shape: + def show: Str = "_" + + def flattenShape: List[Shape] = this :: Nil + + def containsUnion: Bool = false + +class ClassTagsTransformer( + val webs: List[Web], + val flowRes: FlowConstraintSolver, + val debug: Bool, +)(using State, Elaborator.Ctx, TL, Raise) extends BlockTransformer(SymbolSubst.Id): + private given fState: FlowAnalysis.State = flowRes.fState + + private val producersInWeb = webs.iterator.flatMap(_.markedProducers).toSet + + private val ctorsByResultId = producersInWeb.toList.groupBy(_.exprId) + + private val patternMatchesByResultId = + flowRes.consumersWithSrcs.iterator.collect: + case patternMatch: Dtor => patternMatch + .toList.groupBy(_.exprId) + + private val shapeTags = MutMap.empty[Shape, Int] + + private val tagField = new syntax.Tree.Ident("__tag$") + + private enum ShapeMatchScope: + case TopLevel, SupportedFunction, NestedFunction, ClassValue + + private def isShapeMatch(path: Path): Bool = + ClassTagsTransformer.isShapeMatch(path) + + private def rejectUnsupportedShapeMatches(program: Program): Unit = + class Checker(val scope: ShapeMatchScope) extends BlockTraverser: + override def applyResult(result: Result): Unit = + result match + case call @ Call(fun, _) if isShapeMatch(fun) => + val errorMessage = scope match + case ShapeMatchScope.TopLevel => + S(msg"shape.match is not supported at the top level.") + case ShapeMatchScope.NestedFunction => + S(msg"shape.match is not supported in nested functions.") + case ShapeMatchScope.ClassValue => + S(msg"shape.match is not supported in class value initializers.") + case ShapeMatchScope.SupportedFunction => N + errorMessage.foreach: message => + summon[Raise].apply(ErrorReport( + message -> call.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + case _ => () + super.applyResult(result) + + override def applyFunDefn(fun: FunDefn): Unit = + // Anonymous functions originate from lambdas and are lifted later. + val functionScope = + if !fun.sym.nameIsMeaningful then ShapeMatchScope.SupportedFunction + else scope match + case ShapeMatchScope.TopLevel => ShapeMatchScope.SupportedFunction + case _ => ShapeMatchScope.NestedFunction + new Checker(functionScope).applyBlock(fun.body) + + override def applyClsLikeDefn(defn: ClsLikeDefn): Unit = + defn.parentPath.foreach(applyPath) + defn.methods.foreach: method => + new Checker(ShapeMatchScope.SupportedFunction).applyBlock(method.body) + new Checker(ShapeMatchScope.ClassValue).applyBlock(defn.preCtor) + new Checker(ShapeMatchScope.ClassValue).applyBlock(defn.ctor) + defn.companion.foreach: companion => + companion.methods.foreach: method => + new Checker(ShapeMatchScope.SupportedFunction).applyBlock(method.body) + new Checker(ShapeMatchScope.ClassValue).applyBlock(companion.ctor) + + new Checker(ShapeMatchScope.TopLevel).applyBlock(program.main) + + // * Allocate a tag for a shape in the web + private def allocateTag(shape: Shape): Int = + shapeTags.getOrElseUpdate(shape, { + val tag = shapeTags.size + if debug then + summon[TL].emitDbg( + s"class-tags transform-phase > allocated tag $tag for ${shape.show}") + tag + }) + + private def validateProducers(producers: List[Ctor]): Unit = + producers match + case head :: rest => + val fields = head.args.map(_._1) + for producer <- rest do + softAssert(producer.ctor === head.ctor && producer.args.map(_._1) === fields, + s"Mismatched polymorphism for ${ClassTagsDebug.showProducer(head)}") + val mutableFields = fields.collect: + case field: TermSymbol if field.k is syntax.MutVal => field + for field <- mutableFields do + summon[Raise].apply(ErrorReport( + msg"Class tags do not support mutable fields yet." -> field.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + case Nil => + softAssert(false, "Missing constructor for an allocation site.") + + private lazy val shapesByProducer: Map[Ctor, List[ClassShape]] = + given visit: Set[ProdStrat] = Set.empty + producersInWeb.iterator.map: producer => + val shapes = shapeOfProducer(producer) match + case shape: ClassShape => + shape.flattenShape(producer.exprId.getResult.toLoc).collect: + case shape: ClassShape => shape + case _ => Nil + producer -> shapes + .toMap + + private lazy val taggedShapesByResultId: Map[ResultId, List[ClassShape -> Int]] = + ctorsByResultId.toList.sortBy(_._1.uid).flatMap: (resultId, producers) => + validateProducers(producers) + val shapes = producers.flatMap(shapesByProducer.getOrElse(_, Nil)).distinct.sortBy(_.show) + val taggedShapes = shapes.map(shape => shape -> allocateTag(shape)) + if taggedShapes.isEmpty then Nil else (resultId -> taggedShapes) :: Nil + .toMap + + private lazy val taggedShapesByProducer: Map[Ctor, List[ClassShape -> Int]] = + val _ = taggedShapesByResultId + shapesByProducer.iterator.map: (producer, shapes) => + producer -> shapes.map(shape => shape -> shapeTags(shape)) + .toMap + + private def getCtorArgs(producer: Ctor) = + producer.exprId.getResult match + case CtorProducer(_, args, _) => + softAssert( + args.size === producer.args.size, + s"Mismatched constructor arguments for ${ClassTagsDebug.showProducer(producer)}", + ) + args + case result => + softAssert( + false, + s"Missing constructor result for ${ClassTagsDebug.showProducer(producer)}: ${result.showDbg}", + ) + Nil + + private def shapeOfProducer(producer: Ctor)(using visit: Set[ProdStrat]): Shape = + if visit.contains(producer) then DynamicShape + else + given next: Set[ProdStrat] = visit + producer + val args = getCtorArgs(producer) + val fieldsOrElements = producer.args.zipWithIndex.map: + case ((field, value), index) => + val original = args.lift(index).map(_.value) + field -> shapeOf(value, original) + producer.ctor match + case cls: ClassLikeSymbol => + val fields = fieldsOrElements.collect: + case (field: TermSymbol, shape) => field -> shape + softAssert( + fields.size === fieldsOrElements.size, + s"Unexpected class fields in ${ClassTagsDebug.showProducer(producer)}", + ) + ClassShape(cls, fields.toMap) + case length: Int => + softAssert( + fieldsOrElements.size === length, + s"Mismatched tuple arity for ${ClassTagsDebug.showProducer(producer)}", + ) + TupleShape(length, fieldsOrElements.map(_._2)) + + private def shapeOf(producer: ProdStrat, original: Opt[Path])(using visit: Set[ProdStrat]): Shape = + original match + case S(lit: Value.Lit) => LitShape(lit) + case _ => producer match + case ctor: Ctor => shapeOfProducer(ctor) + case variable: StratVar => + if visit.contains(variable) then DynamicShape + else + given next: Set[ProdStrat] = visit + variable + UnionShape.mkUnion: + variable.lowerBounds.map: lowerBound => + shapeOf(lowerBound, N) + case _ => DynamicShape + + // * Get all (shape, tag) pair of the given scrutinee + private def taggedShapesOfMatchScrutinee(matchResultId: ResultId): List[Shape -> Int] = + val taggedShapes = patternMatchesByResultId.getOrElse(matchResultId, Nil).iterator + .flatMap(_.srcs) + .collect: + case ctor: Ctor => ctor + .toList.distinct.flatMap: ctor => + taggedShapesByProducer.getOrElse(ctor, Nil) + taggedShapes.distinct.sortBy(_._2) // sort to avoid changing debug printing everytime + + private def bindResult(result: Result)(k: Path => Block): Block = result match + case path: Path => k(path) + case result => + val symbol = new TempSymbol(N, erasedType = result.erasedValueType, "tmp") + val reference = symbol.asSimpleRef.withLocOf(result) + Scoped(Set.single(symbol), Assign(symbol, result, k(reference))) + + private def assignTag(instance: Path, tag: Int)(next: Block): Block = // TODO: make __tag$ a real field and fill the symbol for selections + AssignField(instance, tagField, Value.Lit(syntax.Tree.IntLit(tag))(N), next)(N) + + private def insertTagForMultiShapes( + result: Result, args: List[Arg], producer: Ctor, taggedShapes: List[ClassShape -> Int] + )(k: Path => Block): Block = + def canCheck(shape: Shape): Bool = + shapeTags.contains(shape) || (shape match + case _: LitShape => true + case TupleShape(_, elements) => elements.forall(canCheck) + case _ => false) + + val outsideArguments = producer.args.iterator.zip(args.iterator).collect: + case ((field: TermSymbol, argumentFlow), argument) + if taggedShapes.exists((shape, _) => !canCheck(shape.fields(field))) => + field -> argument.value + .toList + if outsideArguments.nonEmpty then + summon[Raise].apply(ErrorReport( + msg"Cannot insert a class tag using constructor arguments that may come from outside." -> result.toLoc :: + outsideArguments.map: (field, argument) => + msg"Field ${ClassTagsDebug.showField(field)} cannot be checked using a class tag." -> argument.toLoc, + source = Diagnostic.Source.Compilation, + )) + + val arguments = producer.args.iterator.map(_._1).zip(args.iterator.map(_.value)).collect: + case (field: TermSymbol, path) => field -> path + .toList + + def checkTagEq(left: Path, right: Path)(k: Path => Block) = + bindResult(Call(State.builtinOpsMap("===").asSimpleRef, (left.asArg :: right.asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun, N))(k) + + def checkShape(argument: Path, shape: Shape)(k: Path => Block) = + shapeTags.get(shape) match + case S(tag) => checkTagEq( + Select(argument, tagField)(N, argument.toLoc)(false), Value.Lit(syntax.Tree.IntLit(tag))(N) + )(k) + case N => shape match + case LitShape(lit) => checkTagEq(argument, lit)(k) + case TupleShape(length, elements) => + val condition = new TempSymbol(N, erasedType = S(ErasedType.Bool), "tmp") + val conditionRef = condition.asSimpleRef.withLocOf(argument) + val elementChecks = elements.zipWithIndex.map: (element, index) => + DynSelect(argument, Value.Lit(syntax.Tree.IntLit(index))(N), true)(argument.toLoc) -> element + val matched = mkConjunction(elementChecks): elementsMatch => + Assign(condition, elementsMatch, End()) + Scoped(Set.single(condition), + new Match(argument, Case.Tup(length, false) -> matched :: Nil, + S(Assign(condition, Value.Lit(syntax.Tree.BoolLit(false))(N), End())), + k(conditionRef))) + case DynamicShape => k(Value.Lit(syntax.Tree.BoolLit(true))(N)) + case _ => lastWords(s"Shape ${shape.show} cannot be checked directly.") + + // * Generate tag checks for each parameter and form a conjunction condition + def mkConjunction(checks: List[Path -> Shape])(k: Path => Block): Block = + def rec(checks: List[Path -> Shape])(k: Path => Block): Block = checks match + case Nil => k(Value.Lit(syntax.Tree.BoolLit(true))(N)) + case (argument, shape) :: Nil => checkShape(argument, shape)(k) + case (argument, shape) :: checks => + checkShape(argument, shape): condition => + condition match + case Value.Lit(syntax.Tree.BoolLit(true)) => rec(checks)(k) + case Value.Lit(syntax.Tree.BoolLit(false)) => k(condition) + case _ => + val result = new TempSymbol(N, erasedType = S(ErasedType.Bool), "tmp") + val reference = result.asSimpleRef.withLocOf(condition) + val matched = rec(checks): remainingCondition => + Assign(result, remainingCondition, End()) + Scoped(Set.single(result), + new Match( + condition, + Case.Lit(syntax.Tree.BoolLit(true)) -> matched :: Nil, + S(Assign(result, Value.Lit(syntax.Tree.BoolLit(false))(N), End())), + k(reference), + )) + // remove conditions that are already true + rec(checks.filterNot: + case (_, DynamicShape) => true + case (argument, LitShape(lit)) => argument === lit + case _ => false)(k) + + def assign(remainingShapes: List[ClassShape -> Int], instance: Path): Block = + remainingShapes match + case (shape, tag) :: remainingShapes => + val checks = arguments.map: (field, argument) => + argument -> shape.fields(field) + mkConjunction(checks): condition => + new Match( + condition, + Case.Lit(syntax.Tree.BoolLit(true)) -> assignTag(instance, tag)(End()) :: Nil, + if remainingShapes.isEmpty then N else S(assign(remainingShapes, instance)), + End(), + ) + case Nil => End() + + bindResult(result): instance => + Begin(assign(taggedShapes, instance), k(instance)) + + private def insertShapeTag( + result: Result, producer: Ctor, taggedShapes: List[ClassShape -> Int] + )(k: Path => Block): Block = + taggedShapes match + case (_, tag) :: Nil => + bindResult(result): instance => + assignTag(instance, tag)(k(instance)) + case _ :: _ => result match + case CtorProducer(_, args, _) => + insertTagForMultiShapes(result, args, producer, taggedShapes)(k) + case _ => + lastWords(s"Missing constructor result for ${ClassTagsDebug.showProducer(producer)}") + case Nil => + lastWords(s"Missing concrete shape for ${ClassTagsDebug.showProducer(producer)}") + + override def applyProgram(program: Program): Program = + if debug then + summon[TL].emitDbg(">>> start class-tags transform-phase") + rejectUnsupportedShapeMatches(program) + val _ = taggedShapesByResultId + val result = super.applyProgram(program) + if debug then + summon[TL].emitDbg("<<< end class-tags transform-phase") + result + + override def applyFunDefn(fun: FunDefn): FunDefn = + val transformer = new BlockTransformerShallow(SymbolSubst.Id): + override def applyBlock(block: Block): Block = + block match // * do not check against Assign, since it can be used to initialize variables encaptured by the shape.match arm lambdas + case AssignField(lhs, _, _, _) => + summon[Raise].apply(ErrorReport( + msg"Class tags do not support set operations yet." -> lhs.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + case AssignDynField(lhs, _, _, _, _) => + summon[Raise].apply(ErrorReport( + msg"Class tags do not support set operations yet." -> lhs.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + case _ => () + super.applyBlock(block) + + // * get the branch body defined as a FunDefn + private def getBranch(path: Path): Opt[FunDefn] = + path.targetSymbol.collect: + case symbol: TermSymbol => symbol + .flatMap(flowRes.preAnalyzer.res.funSymToFunDefn.get) + + // * Generate branch based on the branch function + private def mkBranch(branch: FunDefn, resultSymbol: TempSymbol): Block = + SymbolRefresher(Map.empty).apply(applyFunBodyLikeBlock(branch.body)).mapReturn: + case Return(result) => Assign(resultSymbol, result, End()) + + private def rewriteShapeMatch(call: Call, scrutinee: Path, branchArgs: List[Arg])(k: Result => Block): Opt[Block] = + val annotation = call.metadata.annotations.collectFirst: + case Annot.MatchShapes(patterns) => patterns + if annotation.isEmpty then + summon[Raise].apply(ErrorReport( + msg"shape.match must be annotated with @matchShapes." -> call.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + annotation.flatMap: patterns => + val branches = branchArgs.map(arg => getBranch(arg.value)) + val malformedReasons = + (if patterns.size =/= branchArgs.size then + msg"The number of @matchShapes patterns (${patterns.size}) does not match the number of shape.match branches (${branchArgs.size})." -> call.toLoc :: Nil + else Nil) ++ + branchArgs.zip(branches).collect: + case (arg, N) => + msg"This shape.match branch does not resolve to a function." -> arg.value.toLoc + if malformedReasons.nonEmpty then + summon[Raise].apply(ErrorReport( + msg"Malformed annotated shape.match call." -> call.toLoc :: malformedReasons, + source = Diagnostic.Source.Compilation, + )) + N + else + val branchDefns = branches.flatten + val namedBranches = branchArgs.zip(branchDefns).collect: + case (arg, branch) if branch.sym.nameIsMeaningful => arg.value + if namedBranches.nonEmpty then + summon[Raise].apply(ErrorReport( + msg"Annotated shape.match branches must be anonymous functions." -> call.toLoc :: + namedBranches.map: branch => + msg"This branch is a named function." -> branch.toLoc, + source = Diagnostic.Source.Compilation, + )) + N + else + val branchesWithParams = branchArgs.zip(branchDefns).collect: + case (arg, branch) + if branch.params.exists(paramList => paramList.params.nonEmpty || paramList.restParam.nonEmpty) => + arg.value + if branchesWithParams.nonEmpty then + summon[Raise].apply(ErrorReport( + msg"Annotated shape.match branches must take no arguments." -> call.toLoc :: + branchesWithParams.map: branch => + msg"This branch takes arguments." -> branch.toLoc, + source = Diagnostic.Source.Compilation, + )) + N + else + val patternShapes = patterns.map(Shape.mkShapeByPattern) + val taggedShapes = taggedShapesOfMatchScrutinee(call.uid) + if debug then + val shownTaggedShapes = + if taggedShapes.isEmpty then "" + else taggedShapes.map((shape, tag) => s"${shape.show}@$tag").mkString(", ") + summon[TL].emitDbg( + s"class-tags transform-phase > match shapes ${patternShapes.map(_.show).mkString(", ")} against $shownTaggedShapes") + if patternShapes.exists(_.containsUnion) then + softAssert(false, "@matchShapes patterns must not contain union shapes.") + N + else + val ambiguousTags = taggedShapes.flatMap: (taggedShape, tag) => + val branchIndices = taggedShape.flattenShape(call.toLoc).flatMap: concreteShape => + patternShapes.zipWithIndex.collect: + case (patternShape, index) if concreteShape <= patternShape => index + .distinct + if branchIndices.size > 1 then S((taggedShape, tag, branchIndices)) else N + if ambiguousTags.nonEmpty then + for (taggedShape, tag, branchIndices) <- ambiguousTags do + val messages = + msg"Shape tag $tag for ${taggedShape.show} can fall into more than one shape.match branch." -> call.toLoc :: + branchIndices.map: index => + msg"It can fall into branch ${index + 1}, matched by ${patternShapes(index).show}." -> patterns(index).toLoc + summon[Raise].apply(ErrorReport( + messages, + source = Diagnostic.Source.Compilation, + )) + N + else + val matchingBranches = patternShapes.zip(branchDefns).flatMap: (patternShape, branch) => + taggedShapes.collect: + case (taggedShape, tag) if taggedShape <= patternShape => + (taggedShape, tag, branch) + val matchedTags = matchingBranches.iterator.map(_._2).toSet + val unmatchedShapes = taggedShapes.filter((_, tag) => !matchedTags.contains(tag)) + if taggedShapes.isEmpty then + summon[Raise].apply(ErrorReport( + msg"Annotated shape.match has no tagged class shapes for its scrutinee." -> call.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + )) + N + else if unmatchedShapes.nonEmpty then + summon[Raise].apply(ErrorReport( + msg"Annotated shape.match does not cover every possible scrutinee shape." -> call.toLoc :: + unmatchedShapes.map: (shape, tag) => + msg"Shape ${shape.show} with tag $tag does not match any @matchShapes pattern." -> call.toLoc, + source = Diagnostic.Source.Compilation, + )) + N + else + val resultSymbol = new TempSymbol(N, erasedType = call.erasedValueType, "shapeMatchResult") + val resultRef = resultSymbol.asSimpleRef.withLocOf(call) + val tagAccess = Select(scrutinee, tagField)(N, scrutinee.toLoc)(false) + val arms = matchingBranches.map: (_, tag, branch) => + Case.Lit(syntax.Tree.IntLit(tag)) -> mkBranch(branch, resultSymbol) + S(Scoped(Set.single(resultSymbol), new Match(tagAccess, arms, N, k(resultRef)))) + + override def applyResult(result: Result)(k: Result => Block): Block = + result match + case call @ Call(fun, (Arg(N, scrutinee) :: branches) :: Nil) if branches.nonEmpty && isShapeMatch(fun) => + // Rewrite annotated shape.match calls + rewriteShapeMatch(call, scrutinee, branches)(k).getOrElse: + super.applyResult(result)(k) + case CtorProducer(_, _, _) => + // Insert tags for instantiations + // TODO: make the tag a real field? + (for + ctors <- ctorsByResultId.get(result.uid) + ctor <- ctors.headOption // we need ctor here only for args, so any can be taken. + taggedShapes <- taggedShapesByResultId.get(result.uid) + yield ctor -> taggedShapes) match + case S((ctor, taggedShapes)) => + super.applyResult(result): transformed => + insertShapeTag(transformed, ctor, taggedShapes)(k) + case N => super.applyResult(result)(k) + case _ => super.applyResult(result)(k) + val body = transformer.applyFunBodyLikeBlock(fun.body) + val transformed = + if body is fun.body then fun + else FunDefn(fun.owner, fun.sym, fun.dSym, fun.params, body)(fun.configOverride, fun.annotations) + super.applyFunDefn(transformed) +end ClassTagsTransformer + + +object ClassTagsTransformer: + private type WebProducer = ProdStrat | WebEntryCollector.EntryPoints + private type WebConsumer = ConcreteCtorConsumer | ProdStrat | WebEntryCollector.EntryPoints + + private def isShapeMatch(path: Path)(using ctx: Elaborator.Ctx): Bool = + path.targetSymbol.flatMap(_.asBlkMember).contains(ctx.builtins.shape.`match`) + + private def getShapeMatchCalls(program: Program)(using Elaborator.Ctx): List[Call] = + val calls = ListBuffer.empty[Call] + val collector = new BlockTraverser: + override def applyResult(result: Result): Unit = + result match + case call: Call if isShapeMatch(call.fun) => calls += call + case _ => () + super.applyResult(result) + collector.applyProgram(program) + calls.toList + + private def mkWeb( + entries: WebEntryCollector.EntryPoints, + entriesByProducer: Map[Ctor, List[WebEntryCollector.EntryPoints]], + entriesByConsumer: Map[ConcreteCtorConsumer, List[WebEntryCollector.EntryPoints]], + ): Web = + val result = FlowWebComputation[WebProducer, WebConsumer]( + producer => producer match + case ctor: Ctor => + val consumers = ctor.dests.iterator.collect: + case consumer: ConcreteCtorConsumer => consumer: WebConsumer + consumers + ++ ctor.args.iterator.map(arg => arg._2: WebConsumer) + ++ entriesByProducer.getOrElse(ctor, Nil) // also connect other entries in the same function to the current web + case variable: StratVar => + variable.lowerBounds.iterator.map(producer => producer: WebConsumer) + case entries: WebEntryCollector.EntryPoints => + entries.producers.iterator.map(producer => producer: WebConsumer) + ++ entries.consumers + case _ => Nil, + consumer => consumer match + case consumer: ConcreteCtorConsumer => + consumer.srcs.iterator.map(producer => producer: WebProducer) + ++ entriesByConsumer.getOrElse(consumer, Nil) // also connect other entries in the same function to the current web + case variable: StratVar => + variable.lowerBounds.iterator.map(producer => producer: WebProducer) + case producer: ProdStrat => (producer: WebProducer) :: Nil + case entries: WebEntryCollector.EntryPoints => (entries: WebProducer) :: Nil, + entries.producers, + entries.consumers, + ) + FlowWebComputation.Result[Ctor, ConcreteCtorConsumer]( + result.markedProducers.collect: + case ctor: Ctor => ctor, + result.markedConsumers.collect: + case consumer: ConcreteCtorConsumer => consumer, + ) + + private def mkWebs( + entryPoints: List[WebEntryCollector.EntryPoints], + shapeMatchResultIds: Set[ResultId], + ) = + val entriesByProducer = entryPoints.iterator // other entrypoints in the same function as the producer + .flatMap(entries => entries.producers.map(_ -> entries)) + .toList.groupMap(_._1)(_._2) + val entriesByConsumer = entryPoints.iterator // other entrypoints in the same function as the consumer + .flatMap(entries => entries.consumers.map(_ -> entries)) + .toList.groupMap(_._1)(_._2) + val coveredProducers = MutSet.empty[Ctor] + val coveredConsumers = MutSet.empty[ConcreteCtorConsumer] + val webs = ListBuffer.empty[Web] + for entries <- entryPoints do + if + (entries.producers.nonEmpty || entries.consumers.nonEmpty) + && !entries.producers.exists(coveredProducers) + && !entries.consumers.exists(coveredConsumers) + then + val web = mkWeb(entries, entriesByProducer, entriesByConsumer) + coveredProducers ++= web.markedProducers + coveredConsumers ++= web.markedConsumers + webs += web + webs.toList.filter: web => + web.markedConsumers.exists: + case patternMatch: Dtor => shapeMatchResultIds.contains(patternMatch.exprId) + case _ => false + + private def logWebs(webs: List[Web])(using tl: TL): Unit = + if webs.nonEmpty then + tl.emitDbg(">>> start class-tags web-computation-phase") + for (web, index) <- webs.zipWithIndex do + val producers = web.markedProducers.toList.sortBy(_.exprId.uid) + val fieldAccesses = web.markedConsumers.collect: + case access: FieldSel => access + val patternMatches = web.markedConsumers.collect: + case patternMatch: Dtor => patternMatch + tl.emitDbg(s"class-tags web-computation-phase > web $index:") + tl.emitDbg(s"class-tags web-computation-phase > producers: ${producers.map(ClassTagsDebug.showProducer).mkString(", ")}") + if fieldAccesses.nonEmpty then + tl.emitDbg(s"class-tags web-computation-phase > field accesses: ${fieldAccesses.toList.sortBy(_.exprId.uid).map(ClassTagsDebug.showFieldAccess).mkString(", ")}") + if patternMatches.nonEmpty then + tl.emitDbg(s"class-tags web-computation-phase > pattern matches: ${patternMatches.toList.sortBy(_.exprId.uid).map(ClassTagsDebug.showPatternMatch).mkString(", ")}") + tl.emitDbg("<<< end class-tags web-computation-phase") + + def apply(p: Program)(using + cfg: Config, + tl: TL, + raise: Raise, + eState: State, + ctx: Elaborator.Ctx, + symbolPrinter: SymbolPrinter, + ): Program = + cfg.classTags match + case N => p + case S(_) if !cfg.noFreeze => // TODO: make the tag a real field and remove this restriction. + raise(ErrorReport( + msg"Class tag insertion requires :noFreeze." -> N :: Nil, + source = Diagnostic.Source.Compilation, + )) + p + case S(dCfg) => + val matchCalls = getShapeMatchCalls(p) + val flowCfg = Config.FlowAnalysisConfig( + debug = false, + mono = dCfg.mono, + trackNonAffine = false, + trackAccumulator = false, + logNonAffine = false, + logAccumulator = false, + ) + val flowAnalysisRes = + FlowAnalysis.mkTraceLogger(flowCfg, "class-tags flow-analysis-phase > ", tl).givenIn: + FlowAnalysis( + p, + mono = flowCfg.mono, + nonAffineTracking = false, + accumulatorTracking = false, + ) + val matchResultIds = + given FlowAnalysis.State = flowAnalysisRes.fState + matchCalls.iterator.map(_.uid).toSet + val collectorTl = new TraceLogger(using tl.debugPrinter): + override def doTrace: Bool = dCfg.debug + override def emitDbg(str: Str): Unit = + tl.emitDbg(s"class-tags collection-phase > $str") + val entryPoints = collectorTl.givenIn: + if dCfg.debug then tl.emitDbg(">>> start class-tags collection-phase") + val result = WebEntryCollector(p, flowAnalysisRes) + if dCfg.debug then tl.emitDbg("<<< end class-tags collection-phase") + result + val webs = mkWebs(entryPoints, matchResultIds) + if dCfg.debug then logWebs(webs) + new ClassTagsTransformer(webs, flowAnalysisRes, dCfg.debug).applyProgram(p) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index f25997f468..3cacbdf7ac 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -61,6 +61,7 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): else prog runPass("ClassParamFlattener")(ClassParamFlattener.apply) runPass("ReflectionInstrumenter")(ReflectionInstrumenter(using summon).apply) + runPass("ClassTagsTransformer")(ClassTagsTransformer.apply) preOptimizeHook(result) // * We run this pass here first, before inlining so that the @tailrec/@tailcall annotations diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala index 2c2634d564..756342ba37 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala @@ -89,6 +89,7 @@ object DeadConstructorElim: tl: TL, raise: Raise, eState: Elaborator.State, + ctx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, eta = false, dpe = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index 1cfdccf131..e345339fe7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -144,6 +144,7 @@ object DeadParamElim: tl: TL, raise: Raise, eState: Elaborator.State, + ctx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, eta = false, dce = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index cad92575d6..3e64af69f9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -157,6 +157,7 @@ object EtaExpansion: tl: TL, raise: Raise, eState: Elaborator.State, + ctx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, dpe = false, dce = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala index 1e87748365..b118384a43 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala @@ -286,6 +286,7 @@ object FlowAnalysisBasedRewrite: tl: TL, raise: Raise, eState: Elaborator.State, + ctx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p)(rewriteWith(p, _)) @@ -301,6 +302,7 @@ object FlowAnalysisBasedRewrite: tl: TL, raise: Raise, eState: Elaborator.State, + ctx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = def mkTl(prefix: Str, debug: Bool) = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index ded1cae497..bdeaec964e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -128,6 +128,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case WasmIntrinsic case DebugPrintStack case ScopeLocally + case ShapeMatch private lazy val specialBuiltinSymbols: Map[BlockMemberSymbol, SpecialBuiltin] = val blt = ctx.builtins Map( @@ -138,6 +139,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): blt.js.try_catch -> SpecialBuiltin.RuntimeIntrinsic("try_catch"), blt.debug.printStack -> SpecialBuiltin.DebugPrintStack, blt.scope.locally -> SpecialBuiltin.ScopeLocally, + blt.shape.`match` -> SpecialBuiltin.ShapeMatch, blt.wasm.plus_impl -> SpecialBuiltin.WasmIntrinsic, blt.wasm.minus_impl -> SpecialBuiltin.WasmIntrinsic, blt.wasm.times_impl -> SpecialBuiltin.WasmIntrinsic, @@ -964,6 +966,13 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): msg"Unsupported form for scope.locally." -> t.toLoc :: Nil, source = Diagnostic.Source.Compilation) + case S(SpecialBuiltin.ShapeMatch) => + if config.classTags.isEmpty then + return fail: + ErrorReport( + msg"Class tag insertion is not enabled." -> + t.toLoc :: Nil, + source = Diagnostic.Source.Compilation) case N => case N => case N => @@ -1563,6 +1572,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): annotations.foreach: case Annot.Untyped() => () + case annot: Annot.MatchShapes => receiver match + case st.App(fun, _) if fun.resolvedSym.flatMap(_.asBlkMember).contains(ctx.builtins.shape.`match`) => () + case _: st.App => warn(annot, S(msg"The @matchShapes annotation only applies to shape.match calls.")) + case _ => warn(annot) case annot: Annot.Trm => receiver match case st.App(Ref(_: BuiltinSymbol), _) => warn(annot) case st.App(_, _) | New(_, _, _) | DynNew(_, _) | Mut(_: New | _: DynNew) => () diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index 6941682f6d..406877ea90 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -78,7 +78,7 @@ object FlowAnalysis: mono: Bool, nonAffineTracking: Bool, accumulatorTracking: Bool, - )(using TraceLogger, Elaborator.State, Raise, SymbolPrinter) = + )(using TraceLogger, Elaborator.State, Elaborator.Ctx, Raise, SymbolPrinter) = given State = new State val pre = new FlowPreAnalyzer(pgrm) val constrCol = new FlowConstraintsCollector(pre, mono, nonAffineTracking, accumulatorTracking) @@ -672,7 +672,7 @@ class FlowConstraintsCollector( val mono: Bool, val nonAffineTracking: Bool, val accumulatorTracking: Bool, -): +)(using ctx: Elaborator.Ctx): given FlowPreAnalyzer = preAnalyzer given Raise = preAnalyzer.raise given fState: FlowAnalysis.State = preAnalyzer.fState @@ -977,6 +977,8 @@ class FlowConstraintsCollector( def processResult(r: Result)(using cc: ConstraintsCollector): ProdStrat = val instId = cc.instId + def isShapeMatch(fun: Path): Bool = + fun.targetSymbol.flatMap(_.asBlkMember).contains(ctx.builtins.shape.`match`) def handleCallLike(callExprId: ResultId, f: Path, args: List[Arg]): ProdStrat = val fStrat = processResult(f) val argsStrat = args.map(a => processResult(a.value)) @@ -1024,6 +1026,13 @@ class FlowConstraintsCollector( cc.constrain(processResult(qual), UnknownCons) args.foreach(arg => cc.constrain(processResult(arg.value), UnknownCons)) UnknownProd + case c@Call(fun, (Arg(N, scrutinee) :: branches) :: Nil) + if isShapeMatch(fun) && branches.nonEmpty && branches.forall(_.spread.isEmpty) => + cc.constrain(processResult(scrutinee), new Dtor(c.uid, instId)) + val matchResult = freshVar("shape_match_res", cc.forFunGroup) + for Arg(_, branch) <- branches do + cc.constrain(processResult(branch), new ConsFun(c.uid, instId)(Nil, matchResult)) + matchResult case c@Call(fun, argss) => argss match case args :: Nil => handleCallLike(c.uid, fun, args) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index b5e6350344..23945cd01a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -352,10 +352,13 @@ object Elaborator: val buffered = assumeObject("buffered") val bufferable = assumeObject("bufferable") val pure = assumeObject("pure") + val matchShapes = assumeObject("matchShapes") object handlers extends VirtualModule(assumeBuiltinMod("handlers")): val await = assumeObject("await").asTrm.get object scope extends VirtualModule(assumeBuiltinMod("scope")): val locally = assumeObject("locally") + object shape extends VirtualModule(assumeBuiltinMod("shape")): + val `match` = assumeObject("match") object runtime extends VirtualModule(assumeBuiltinMod("runtime")): val suspend = assumeObject("suspend") val handle_suspension = assumeObject("handle_suspension") @@ -669,6 +672,8 @@ extends Importer: case App(Ident("config"), Tup(args)) => val modify = ConfigParser.parseOverrides(args) S(Annot.Config(modify)(tree.toLoc)) + case App(Ident("matchShapes"), Tup(patterns)) => + S(Annot.MatchShapes(patterns.map(pattern))(tree.toLoc)) case App(Ident("affine"), Tup(IntLit(whichParamList) :: Nil)) => S(Annot.Affine(whichParamList.toInt)(tree.toLoc)) case _ => term(tree) match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 947c56a25b..bfed277f64 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -38,6 +38,7 @@ enum Annot extends Located: case RaiseEffects()(val toLoc: Opt[Loc]) // Whether the function is guaranteed to not raise effects. case Pure()(val toLoc: Opt[Loc]) + case MatchShapes(patterns: Ls[Pattern])(val toLoc: Opt[Loc]) case Config(modify: hkmc2.Config => hkmc2.Config)(val toLoc: Opt[Loc]) // Marks if a function or lambda is one-shot, i.e. called at most once. // Functions with multiple parameter lists are considered here as a chain of @@ -57,6 +58,7 @@ enum Annot extends Located: def subTerms: Vector[Term] = this match case Trm(trm) => Vector.single(trm) + case MatchShapes(patterns) => patterns.iterator.flatMap(_.subTerms).toVector case _: Modifier | Untyped() | TailRec() | TailCall() | Inline() | NoInline() | Generator() | Async() | RaiseEffects() | Pure() | _: Config | _: Affine => Vector.empty @@ -72,6 +74,7 @@ enum Annot extends Located: case Affine(n) => doc"@affine($n)" case Modifier(mod) => doc"@${mod.name}" case Pure() => doc"@pure" + case MatchShapes(_) => doc"@matchShapes" case Trm(trm) => doc"@${trm.show}" case Config(_) => doc"@config(...)" @@ -1494,4 +1497,3 @@ trait BlkImpl: (stats ::: (res match case Lit(Tree.UnitLit(false)) => Nil case res => res :: Nil)).map(_.show).mkDocument(doc", # ") - diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Annotations.mls b/hkmc2/shared/src/test/mlscript/class-tags/Annotations.mls new file mode 100644 index 0000000000..919a7fd9a3 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Annotations.mls @@ -0,0 +1,75 @@ +:classTags debug mono +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +class C(val x) +class D(val x) +class E(val x) +class F(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ge +fun foo(x) = @matchShapes(C(D(_)), C(E(1)), F(_)) shape.match of x, + () => x.C#x.D#x + 1, + () => x.C#x.E#x - 1, + () => x.F#x +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > match shapes C(x: D(x: _)), C(x: E(x: 1)), F(x: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.18: fun foo(x) = @matchShapes(C(D(_)), C(E(1)), F(_)) shape.match of x, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.19: () => x.C#x.D#x + 1, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.20: () => x.C#x.E#x - 1, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.21: () => x.F#x +//│ ╙── ^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +:w +@matchShapes(C(_)) 1 +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.39: @matchShapes(C(_)) 1 +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ╟── This annotation is not supported on integer literal terms. +//│ ║ l.39: @matchShapes(C(_)) 1 +//│ ╙── ^ +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:w +@matchShapes(C(_)) fun foo(x) = x +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.53: @matchShapes(C(_)) fun foo(x) = x +//│ ╙── ^^^^^^^^^^^^^^^^^^ +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:w +fun foo(x) = x +@matchShapes(C(_)) foo(42) +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.65: @matchShapes(C(_)) foo(42) +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ╟── The @matchShapes annotation only applies to shape.match calls. +//│ ║ l.65: @matchShapes(C(_)) foo(42) +//│ ╙── ^^^^^^^ +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase diff --git a/hkmc2/shared/src/test/mlscript/class-tags/BadShapes.mls b/hkmc2/shared/src/test/mlscript/class-tags/BadShapes.mls new file mode 100644 index 0000000000..fa75ea0c4a --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/BadShapes.mls @@ -0,0 +1,467 @@ +:classTags debug mono +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +class C(val x) +class D(val x) +class Box(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ge +let topBox = new Box(0) +@matchShapes(Box(_)) shape.match of topBox, () => 1 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] shape.match is not supported at the top level. +//│ ║ l.20: @matchShapes(Box(_)) shape.match of topBox, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase +//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'match') +//│ topBox = Box(0) + + +:ge +class Initialized() with + val value = + let box = new Box(0) + @matchShapes(Box(_)) shape.match of box, () => 1 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] shape.match is not supported in class value initializers. +//│ ║ l.36: @matchShapes(Box(_)) shape.match of box, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +:ge +fun localFunction() = + fun inner() = + let value = new Box(0) + @matchShapes(Box(_)) shape.match of value, () => 1 + inner() +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] shape.match is not supported in nested functions. +//│ ║ l.50: @matchShapes(Box(_)) shape.match of value, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ class-tags transform-phase > match shapes Box(x: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.50: @matchShapes(Box(_)) shape.match of value, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + +:ge +fun unannotated() = + let value = new Box(0) + shape.match of value, () => 1 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in unannotated +//│ class-tags collection-phase > track consumption at match@result in unannotated +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: 0) +//│ ╔══[COMPILATION ERROR] shape.match must be annotated with @matchShapes. +//│ ║ l.67: shape.match of value, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + +:ge +fun mismatchedBranches(t) = + @matchShapes(C(_), D(_)) shape.match of t, + () => 0 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] Malformed annotated shape.match call. +//│ ║ l.86: @matchShapes(C(_), D(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.87: () => 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── The number of @matchShapes patterns (2) does not match the number of shape.match branches (1). +//│ ║ l.86: @matchShapes(C(_), D(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.87: () => 0 +//│ ╙── ^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +:ge +fun branchWithParameter(t) = + @matchShapes(C(_)) shape.match of t, + x => x +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] Annotated shape.match branches must take no arguments. +//│ ║ l.106: @matchShapes(C(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.107: x => x +//│ ║ ^^^^^^^^^^ +//│ ╙── This branch takes arguments. +//│ <<< end class-tags transform-phase + + +:ge +fun missingPatternArgument(t) = + @matchShapes(C) shape.match of t, + () => 0 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] Expected constructor arity 1 in @matchShapes pattern for C, but found 0. +//│ ║ l.122: @matchShapes(C) shape.match of t, +//│ ╙── ^ +//│ class-tags transform-phase > match shapes _ against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.122: @matchShapes(C) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.123: () => 0 +//│ ╙── ^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +:ge +fun foo() = + let t = new C(0) + @matchShapes(D(_)) shape.match of t, + () => t.D#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: 0) +//│ class-tags transform-phase > match shapes D(x: _) against C(x: 0)@0 +//│ ╔══[COMPILATION ERROR] Annotated shape.match does not cover every possible scrutinee shape. +//│ ║ l.142: @matchShapes(D(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.143: () => t.D#x +//│ ║ ^^^^^^^^^^^^^^^^ +//│ ╟── Shape C(x: 0) with tag 0 does not match any @matchShapes pattern. +//│ ║ l.142: @matchShapes(D(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.143: () => t.D#x +//│ ╙── ^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +:ge +fun overlap() = + let value = new Box(0) + @matchShapes(Box(_), _) shape.match of value, () => 1, () => 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in overlap +//│ class-tags collection-phase > track consumption at match@result in overlap +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: 0) +//│ class-tags transform-phase > match shapes Box(x: _), _ against Box(x: 0)@0 +//│ ╔══[COMPILATION ERROR] Shape tag 0 for Box(x: 0) can fall into more than one shape.match branch. +//│ ║ l.172: @matchShapes(Box(_), _) shape.match of value, () => 1, () => 2 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── It can fall into branch 1, matched by Box(x: _). +//│ ║ l.172: @matchShapes(Box(_), _) shape.match of value, () => 1, () => 2 +//│ ║ ^^^^^ +//│ ╟── It can fall into branch 2, matched by _. +//│ ║ l.172: @matchShapes(Box(_), _) shape.match of value, () => 1, () => 2 +//│ ╙── ^ +//│ <<< end class-tags transform-phase + + + +:ge +fun unknownScrutinee(t) = + @matchShapes(C(_)) shape.match of t, + () => 0 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > match shapes C(x: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.200: @matchShapes(C(_)) shape.match of t, +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.201: () => 0 +//│ ╙── ^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + + +class MutableBox(mut val x) +class A(val x) +class B(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +// A tag derived from constructor arguments becomes stale after a field write. +// The flow analysis marks the escape, but web traversal still admits this producer. +:ge +fun mutatedField() = + let value = new MutableBox(new A(0)) + set value.MutableBox#x = new B(0) + @matchShapes(MutableBox(A(_)), MutableBox(B(_))) shape.match of value, () => 1, () => 2 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] Class tags do not support set operations yet. +//│ ║ l.229: set value.MutableBox#x = new B(0) +//│ ╙── ^^^^^ +//│ class-tags transform-phase > match shapes MutableBox(x: A(x: _)), MutableBox(x: B(x: _)) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.230: @matchShapes(MutableBox(A(_)), MutableBox(B(_))) shape.match of value, () => 1, () => 2 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +// Constructor bodies can change fields even before the tag is first assigned. +:ge +class Altered(mut val x) with + set x = new B(0) +fun constructorMutation() = + let value = new Altered(new A(0)) + @matchShapes(Altered(A(_)), Altered(B(_))) shape.match of value, () => 1, () => 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Altered@result in constructorMutation +//│ class-tags collection-phase > track consumption at match@result in constructorMutation +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: A@result, Altered@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for A(x: 0) +//│ ╔══[COMPILATION ERROR] Class tags do not support mutable fields yet. +//│ ║ l.246: class Altered(mut val x) with +//│ ╙── ^ +//│ class-tags transform-phase > allocated tag 1 for Altered(x: A(x: 0)) +//│ class-tags transform-phase > match shapes Altered(x: A(x: _)), Altered(x: B(x: _)) against Altered(x: A(x: 0))@1 +//│ <<< end class-tags transform-phase + + +:ge +fun dynamicField(flag, external) = + let field = if flag then new A(0) else external + let value = new Box(field) + @matchShapes(Box(_)) shape.match of value, () => 1 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in dynamicField +//│ class-tags collection-phase > track consumption at match@result in dynamicField +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: A@result, Box@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for A(x: 0) +//│ class-tags transform-phase > allocated tag 1 for Box(x: A(x: 0)) +//│ class-tags transform-phase > allocated tag 2 for Box(x: _) +//│ ╔══[COMPILATION ERROR] Cannot insert a class tag using constructor arguments that may come from outside. +//│ ║ l.273: let value = new Box(field) +//│ ║ ^^^^^^^^^^^^^^ +//│ ╟── Field x cannot be checked using a class tag. +//│ ║ l.273: let value = new Box(field) +//│ ╙── ^^^^^ +//│ class-tags transform-phase > match shapes Box(x: _) against Box(x: A(x: 0))@1, Box(x: _)@2 +//│ <<< end class-tags transform-phase + + + +:ge +fun unknownAlternative(flag, external) = + let value = if flag > 0 then new A(0) else if flag < 0 then new B(0) else external + @matchShapes(A(_), B(_)) shape.match of value, () => 1, () => 2 +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > match shapes A(x: _), B(x: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.302: @matchShapes(A(_), B(_)) shape.match of value, () => 1, () => 2 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +// A consumer must not pull a constructor that escapes the function back into a web. +:ge +fun escapingAlternative() = + let value = new A(0) + let _ = @matchShapes(A(_)) shape.match of value, () => 1 + value +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > match shapes A(x: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.317: let _ = @matchShapes(A(_)) shape.match of value, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase + + +// this would not happen in the generated code +// since bar(false) should be specialized and become another function +:ge +private fun bar(x) = + if x then new C(bar(false)) else new D(0) +fun foo() = + let value = bar(true) + @matchShapes(C(_), D(_)) shape.match of value, + () => 1 + () => 0 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result in bar +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, D@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: D(x: 0)) +//│ class-tags transform-phase > allocated tag 1 for C(x: _) +//│ class-tags transform-phase > allocated tag 2 for D(x: 0) +//│ ╔══[COMPILATION ERROR] Cannot insert a class tag using constructor arguments that may come from outside. +//│ ║ l.333: if x then new C(bar(false)) else new D(0) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╙── Field x cannot be checked using a class tag. +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: D(x: 0))@0, C(x: _)@1, D(x: 0)@2 +//│ <<< end class-tags transform-phase + + +:ge +private fun curried()() = 42 +fun curriedBranch() = + let value = new Box(0) + let result = @matchShapes(Box(_)) shape.match of value, curried + result() +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in curriedBranch +//│ class-tags collection-phase > track consumption at match@result in curriedBranch +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: 0) +//│ ╔══[COMPILATION ERROR] Annotated shape.match branches must be anonymous functions. +//│ ║ l.364: let result = @matchShapes(Box(_)) shape.match of value, curried +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── This branch is a named function. +//│ ║ l.364: let result = @matchShapes(Box(_)) shape.match of value, curried +//│ ╙── ^^^^^^^ +//│ <<< end class-tags transform-phase + + +:ge +private fun f() = 42 +fun uncurriedBranch() = + let value = new C(0) + let result = @matchShapes(C(_)) shape.match of value, f + result +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result in uncurriedBranch +//│ class-tags collection-phase > track consumption at match@result in uncurriedBranch +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: 0) +//│ ╔══[COMPILATION ERROR] Annotated shape.match branches must be anonymous functions. +//│ ║ l.390: let result = @matchShapes(C(_)) shape.match of value, f +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── This branch is a named function. +//│ ║ l.390: let result = @matchShapes(C(_)) shape.match of value, f +//│ ╙── ^ +//│ <<< end class-tags transform-phase + +:ge +let remaining = 1 +private fun recursiveBranch() = + if remaining > 0 then + set remaining = remaining - 1 + let value = new Box(0) + @matchShapes(Box(_)) shape.match of value, recursiveBranch + else 42 +fun recursiveMatch() = recursiveBranch() +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in recursiveBranch +//│ class-tags collection-phase > track consumption at match@result in recursiveBranch +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: 0) +//│ ╔══[COMPILATION ERROR] Annotated shape.match branches must be anonymous functions. +//│ ║ l.417: @matchShapes(Box(_)) shape.match of value, recursiveBranch +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── This branch is a named function. +//│ ║ l.417: @matchShapes(Box(_)) shape.match of value, recursiveBranch +//│ ╙── ^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase +//│ remaining = 1 + + +// Reusing a two-way union in nine fields would flatten to 2^9 concrete shapes. +:ge +class TooManyShapes(val a, val b, val c, val d, val e, val f, val g, val h, val i) +fun flattenBudget(flag) = + let field = if flag then new A(0) else new B(0) + let value = new TooManyShapes(field, field, field, field, field, field, field, field, field) + @matchShapes(TooManyShapes(_, _, _, _, _, _, _, _, _)) shape.match of value, () => 1 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of TooManyShapes@result in flattenBudget +//│ class-tags collection-phase > track consumption at match@result in flattenBudget +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: A@result, B@result, TooManyShapes@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ ╔══[COMPILATION ERROR] Flattening this shape would produce more than 256 alternatives. +//│ ║ l.446: let value = new TooManyShapes(field, field, field, field, field, field, field, field, field) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ class-tags transform-phase > allocated tag 0 for A(x: 0) +//│ class-tags transform-phase > allocated tag 1 for B(x: 0) +//│ class-tags transform-phase > match shapes TooManyShapes(b: _, g: _, c: _, f: _, i: _, e: _, a: _, h: _, d: _) against +//│ ╔══[COMPILATION ERROR] Annotated shape.match has no tagged class shapes for its scrutinee. +//│ ║ l.447: @matchShapes(TooManyShapes(_, _, _, _, _, _, _, _, _)) shape.match of value, () => 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ <<< end class-tags transform-phase diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Basic.mls b/hkmc2/shared/src/test/mlscript/class-tags/Basic.mls new file mode 100644 index 0000000000..8b416e02aa --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Basic.mls @@ -0,0 +1,612 @@ +:classTags debug mono +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +class Foo(val x, val y) +class Bar(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +fun foo(x, y) = new Foo(x, y) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +fun foo(x, y) = + let f = new Foo(x, y) + @matchShapes(Foo(_, _)) shape.match of f, () => new Bar(f.Foo#x + f.Foo#y) +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Foo@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Foo.y@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: _) against Foo(x: _, y: _)@0 +//│ <<< end class-tags transform-phase + + +private fun bar(x) = new Bar(x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +module Baz with + fun bar(x) = new Bar(x) + private fun barr(x) = + let f = new Foo(x, x) + if f is + Foo(x, y) then new Bar(x + y) + fun baz(x) = + let f = new Foo(x, x) + @matchShapes(Foo(_, _)) shape.match of f, () => new Bar(f.Foo#x + f.Foo#y) +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Foo@result in Baz.barr +//│ class-tags collection-phase > track consumption at Foo.x@result, Foo.y@result in Baz.barr +//│ class-tags collection-phase > track construction of Foo@result in Baz.baz +//│ class-tags collection-phase > track consumption at match@result in Baz.baz +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Foo.y@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: _) against Foo(x: _, y: _)@0 +//│ <<< end class-tags transform-phase + + +:ssjs +fun foo(x, y) = + let f = new Foo(x, y) + bar(f) * baz(f) +private fun bar(f) = + @matchShapes(Foo(_, _)) shape.match of f, () => f.Foo#x + f.Foo#y +private fun baz(f) = + @matchShapes(Foo(_, _)) shape.match of f, () => f.Foo#x - f.Foo#y +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Foo@result in foo +//│ class-tags collection-phase > track consumption at match@result in bar +//│ class-tags collection-phase > track consumption at match@result in baz +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Foo.y@result, Foo.x@result, Foo.y@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: _) against Foo(x: _, y: _)@0 +//│ class-tags transform-phase > match shapes Foo(x: _, y: _) against Foo(x: _, y: _)@0 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let bar1, foo2, baz; +//│ foo2 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp, shapeMatchResult, shapeMatchResult1; +//│ tmp = (new Foo1.class(x, y)); +//│ tmp.__tag$ = 0; +//│ if (tmp.__tag$ === 0) { +//│ shapeMatchResult1 = tmp.x + tmp.y; +//│ } +//│ if (tmp.__tag$ === 0) { +//│ shapeMatchResult = tmp.x - tmp.y; +//│ return shapeMatchResult1 * shapeMatchResult +//│ } +//│ return shapeMatchResult1 * undefined; +//│ }; +//│ bar1 = function bar(f) { +//│ runtime.checkArgs("bar", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (f.__tag$ === 0) { +//│ shapeMatchResult = f.x + f.y; +//│ } +//│ return shapeMatchResult +//│ }; +//│ baz = function baz(f) { +//│ runtime.checkArgs("baz", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (f.__tag$ === 0) { shapeMatchResult = f.x - f.y; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:expect 3 +foo(2, 1) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 3 + + +:ssjs +fun foo(x, y) = + let f = new Foo(x, y) + baz(f) +fun bar(x) = + let b = new Bar(x) + baz(b) +private fun baz(t) = + @matchShapes(Foo(_, _), Bar(_)) shape.match of t, + () => t.Foo#x + t.Foo#y + () => t.Bar#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Foo@result in foo +//│ class-tags collection-phase > track construction of Bar@result in bar +//│ class-tags collection-phase > track consumption at match@result in baz +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Foo.y@result, Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: _) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: _), Bar(x: _) against Foo(x: _, y: _)@0, Bar(x: _)@1 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let bar2, foo3, baz1; +//│ foo3 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp1; +//│ tmp1 = (new Foo1.class(x, y)); +//│ tmp1.__tag$ = 0; +//│ if (tmp1.__tag$ === 0) { +//│ return tmp1.x + tmp1.y +//│ } else if (tmp1.__tag$ === 1) { +//│ return tmp1.x +//│ } +//│ return runtime.Unit; +//│ }; +//│ bar2 = function bar(x) { +//│ runtime.checkArgs("bar", 1, true, arguments.length); +//│ let tmp1; +//│ tmp1 = (new Bar1.class(x)); +//│ tmp1.__tag$ = 1; +//│ if (tmp1.__tag$ === 0) { +//│ return tmp1.x + tmp1.y +//│ } else if (tmp1.__tag$ === 1) { +//│ return tmp1.x +//│ } +//│ return runtime.Unit; +//│ }; +//│ baz1 = function baz(t) { +//│ runtime.checkArgs("baz", 1, true, arguments.length); +//│ let shapeMatchResult2; +//│ if (t.__tag$ === 0) { +//│ shapeMatchResult2 = t.x + t.y; +//│ } else if (t.__tag$ === 1) { shapeMatchResult2 = t.x; } +//│ return shapeMatchResult2 +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class Some(val x) +object None +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +private fun foo(x) = + if x > 0 then new Some(x) else None +fun bar(x) = + let t = foo(x) + @matchShapes(Some(_), None) shape.match of t, + () => t.Some#x, + () => 0 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Some@result, None@None in foo +//│ class-tags collection-phase > track consumption at match@result in bar +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Some@result, None@None +//│ class-tags web-computation-phase > field accesses: Some.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Some(x: _) +//│ class-tags transform-phase > allocated tag 1 for None +//│ class-tags transform-phase > match shapes Some(x: _), None against Some(x: _)@0, None@1 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let bar3, foo4; +//│ foo4 = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ let scrut; +//│ scrut = x > 0; +//│ if (scrut === true) { +//│ let tmp1; +//│ tmp1 = (new Some1.class(x)); +//│ tmp1.__tag$ = 0; +//│ return tmp1 +//│ } +//│ None1.__tag$ = 1; +//│ return None1; +//│ }; +//│ bar3 = function bar(x) { +//│ runtime.checkArgs("bar", 1, true, arguments.length); +//│ let inlinedVal, scrut; +//│ scrut = x > 0; +//│ if (scrut === true) { +//│ let tmp1; +//│ tmp1 = (new Some1.class(x)); +//│ tmp1.__tag$ = 0; +//│ inlinedVal = tmp1; +//│ } else { +//│ None1.__tag$ = 1; +//│ inlinedVal = None1; +//│ } +//│ if (inlinedVal.__tag$ === 0) { +//│ return inlinedVal.x +//│ } else if (inlinedVal.__tag$ === 1) { return 0 } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun callCtor(x, y) = + let f = Foo(x, y) + @matchShapes(Foo(_, _)) shape.match of f, () => f.Foo#x + f.Foo#y +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Foo@result in callCtor +//│ class-tags collection-phase > track consumption at match@result in callCtor +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Foo.y@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: _) against Foo(x: _, y: _)@0 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let callCtor; +//│ callCtor = function callCtor(x, y) { +//│ runtime.checkArgs("callCtor", 2, true, arguments.length); +//│ let tmp1; +//│ tmp1 = runtime.checkCall(Foo1(x, y)); +//│ tmp1.__tag$ = 0; +//│ if (tmp1.__tag$ === 0) { return tmp1.x + tmp1.y } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun foo(x, y) = + let f = [1, 2, 3] + if f is + [x, y, z] then new Bar(x + y + z) +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of tup(size 3)@result in foo +//│ class-tags collection-phase > track consumption at tup(size 3).2@result, tup(size 3).1@result, tup(size 3).0@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo5; +//│ foo5 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let f, element2$, element1$, element0$, tmp1, tmp2; +//│ f = ([ +//│ 1, +//│ 2, +//│ 3 +//│ ]); +//│ if (runtime.Tuple.isArrayLike(f) && f.length === 3) { +//│ element0$ = runtime.checkCall(runtime.Tuple.get(f, 0)); +//│ element1$ = runtime.checkCall(runtime.Tuple.get(f, 1)); +//│ element2$ = runtime.checkCall(runtime.Tuple.get(f, 2)); +//│ tmp1 = element0$ + element1$; +//│ tmp2 = tmp1 + element2$; +//│ return (new Bar1.class(tmp2)) +//│ } +//│ throw (new globalThis.Error("match error")); +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun foo(x, y) = + let t = new Some(x) + let p = @matchShapes(Some(_), None) shape.match of t, () => new Bar(t.Some#x), () => new Bar(0) + @matchShapes(Bar(_)) shape.match of p, () => p.Bar#x + 1 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Some@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Some@result, Bar@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Some.x@result, Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Some(x: _) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: _) +//│ class-tags transform-phase > allocated tag 2 for Bar(x: 0) +//│ class-tags transform-phase > match shapes Some(x: _), None against Some(x: _)@0 +//│ class-tags transform-phase > match shapes Bar(x: _) against Bar(x: _)@1, Bar(x: 0)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo6; +//│ foo6 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp1, shapeMatchResult2; +//│ tmp1 = (new Some1.class(x)); +//│ tmp1.__tag$ = 0; +//│ if (tmp1.__tag$ === 0) { +//│ let tmp2; +//│ tmp2 = (new Bar1.class(tmp1.x)); +//│ tmp2.__tag$ = 1; +//│ shapeMatchResult2 = tmp2; +//│ } +//│ if (shapeMatchResult2.__tag$ === 1) { +//│ return shapeMatchResult2.x + 1 +//│ } else if (shapeMatchResult2.__tag$ === 2) { +//│ return shapeMatchResult2.x + 1 +//│ } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class C(val x) +class D(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ssjs +fun foo(x) = + let t = if x then new Bar(new C(0)) else new Bar(new D(1)) + @matchShapes(Bar(_)) shape.match of t, () => t.Bar#x.x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, Bar@result, D@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: 0) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: C(x: 0)) +//│ class-tags transform-phase > allocated tag 2 for D(x: 1) +//│ class-tags transform-phase > allocated tag 3 for Bar(x: D(x: 1)) +//│ class-tags transform-phase > match shapes Bar(x: _) against Bar(x: C(x: 0))@1, Bar(x: D(x: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo7; +//│ foo7 = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ let tmp1; +//│ if (x === true) { +//│ let tmp2, tmp3; +//│ tmp2 = (new C1.class(0)); +//│ tmp2.__tag$ = 0; +//│ tmp3 = (new Bar1.class(tmp2)); +//│ tmp3.__tag$ = 1; +//│ tmp1 = tmp3; +//│ } else { +//│ let tmp2, tmp3; +//│ tmp2 = (new D1.class(1)); +//│ tmp2.__tag$ = 2; +//│ tmp3 = (new Bar1.class(tmp2)); +//│ tmp3.__tag$ = 3; +//│ tmp1 = tmp3; +//│ } +//│ if (tmp1.__tag$ === 1) { +//│ return runtime.checkSelect(tmp1.x.x, "x", tmp1.x) +//│ } else if (tmp1.__tag$ === 3) { +//│ return runtime.checkSelect(tmp1.x.x, "x", tmp1.x) +//│ } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun foo(x) = + let t = if x then new C(0) else new C(0) + @matchShapes(C(_)) shape.match of t, () => t.C#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, C@result +//│ class-tags web-computation-phase > field accesses: C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: 0) +//│ class-tags transform-phase > match shapes C(x: _) against C(x: 0)@0 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo8; +//│ foo8 = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ let tmp1; +//│ if (x === true) { +//│ let tmp2; +//│ tmp2 = (new C1.class(0)); +//│ tmp2.__tag$ = 0; +//│ tmp1 = tmp2; +//│ } else { +//│ let tmp2; +//│ tmp2 = (new C1.class(0)); +//│ tmp2.__tag$ = 0; +//│ tmp1 = tmp2; +//│ } +//│ if (tmp1.__tag$ === 0) { return tmp1.x } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun foo(x) = + let tuple = if x then [1, 3] else [1, 4] + let value = new Bar(tuple) + @matchShapes(Bar([1, 3]), Bar([1, 4])) shape.match of value, + () => 3, + () => 4 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result in foo +//│ class-tags collection-phase > track consumption at match@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: tup(size 2)@result, tup(size 2)@result, Bar@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Bar(x: tup(size 2)(1, 3)) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: tup(size 2)(1, 4)) +//│ class-tags transform-phase > match shapes Bar(x: tup(size 2)(1, 3)), Bar(x: tup(size 2)(1, 4)) against Bar(x: tup(size 2)(1, 3))@0, Bar(x: tup(size 2)(1, 4))@1 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo9; +//│ foo9 = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ let tmp1, tmp2, tmp3; +//│ if (x === true) { +//│ tmp1 = ([ +//│ 1, +//│ 3 +//│ ]); +//│ } else { +//│ tmp1 = ([ +//│ 1, +//│ 4 +//│ ]); +//│ } +//│ tmp2 = (new Bar1.class(tmp1)); +//│ if (runtime.Tuple.isArrayLike(tmp1) && tmp1.length === 2) { +//│ let tmp4, tmp5; +//│ tmp4 = tmp1.at(0) === 1; +//│ if (tmp4 === true) { +//│ let tmp6; +//│ tmp6 = tmp1.at(1) === 3; +//│ tmp5 = tmp6; +//│ } else { +//│ tmp5 = false; +//│ } +//│ tmp3 = tmp5; +//│ } else { +//│ tmp3 = false; +//│ } +//│ if (tmp3 === true) { +//│ tmp2.__tag$ = 0; +//│ } else { +//│ let tmp4; +//│ if (runtime.Tuple.isArrayLike(tmp1) && tmp1.length === 2) { +//│ let tmp5, tmp6; +//│ tmp5 = tmp1.at(0) === 1; +//│ if (tmp5 === true) { +//│ let tmp7; +//│ tmp7 = tmp1.at(1) === 4; +//│ tmp6 = tmp7; +//│ } else { +//│ tmp6 = false; +//│ } +//│ tmp4 = tmp6; +//│ } else { +//│ tmp4 = false; +//│ } +//│ if (tmp4 === true) { +//│ tmp2.__tag$ = 1; +//│ } +//│ } +//│ if (tmp2.__tag$ === 0) { +//│ return 3 +//│ } else if (tmp2.__tag$ === 1) { return 4 } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:expect 3 +foo(true) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 3 + + +:expect 4 +foo(false) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 4 + + +// do not insert a tag for t, since it is not matched by shape.match +:ssjs +fun foo(x) = + let t = if x then new Bar(new C(0)) else new Bar(new D(1)) + if t is + Bar then t.Bar#x.x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result in foo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo10; +//│ foo10 = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ let tmp1, tmp2, tmp3; +//│ if (x === true) { +//│ tmp1 = (new C1.class(0)); +//│ tmp2 = (new Bar1.class(tmp1)); +//│ } else { +//│ tmp3 = (new D1.class(1)); +//│ tmp2 = (new Bar1.class(tmp3)); +//│ } +//│ if (tmp2 instanceof Bar1.class) { +//│ return runtime.checkSelect(tmp2.x.x, "x", tmp2.x) +//│ } +//│ throw (new globalThis.Error("match error")); +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Func.mls b/hkmc2/shared/src/test/mlscript/class-tags/Func.mls new file mode 100644 index 0000000000..f10c52ed4d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Func.mls @@ -0,0 +1,720 @@ +:classTags debug mono +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +class Foo(val x, val y) +class Bar(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ssjs +fun f(x) = + let t = if x then new Foo(x, 0) else new Bar(x) + @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => g(t) + () => h(t) +private fun g(t) = t.Foo#x +private fun h(t) = t.Bar#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ class-tags collection-phase > track consumption at Foo.x@result in g +//│ class-tags collection-phase > track consumption at Bar.x@result in h +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: 0) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: _, y: 0)@0, Bar(x: _)@1 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f, g, h; +//│ f = function f(x) { +//│ runtime.checkArgs("f", 1, true, arguments.length); +//│ let tmp; +//│ if (x === true) { +//│ let tmp1; +//│ tmp1 = (new Foo1.class(x, 0)); +//│ tmp1.__tag$ = 0; +//│ tmp = tmp1; +//│ } else { +//│ let tmp1; +//│ tmp1 = (new Bar1.class(x)); +//│ tmp1.__tag$ = 1; +//│ tmp = tmp1; +//│ } +//│ if (tmp.__tag$ === 0) { +//│ return tmp.x +//│ } else if (tmp.__tag$ === 1) { +//│ return tmp.x +//│ } +//│ return runtime.Unit; +//│ }; +//│ g = function g(t) { +//│ runtime.checkArgs("g", 1, true, arguments.length); +//│ return t.x +//│ }; +//│ h = function h(t) { +//│ runtime.checkArgs("h", 1, true, arguments.length); +//│ return t.x +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class C(val x) +class D(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +fun f(x, y) = + let t = if x then new Foo(new C(y), 0) else new Bar(new D(y)) + @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => + let m = t.Foo#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 + () => + let m = t.Bar#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result, Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, Foo@result, D@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, C.x@result, Bar.x@result, D.x@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: _) +//│ class-tags transform-phase > allocated tag 1 for Foo(x: C(x: _), y: 0) +//│ class-tags transform-phase > allocated tag 2 for D(x: _) +//│ class-tags transform-phase > allocated tag 3 for Bar(x: D(x: _)) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: C(x: _), y: 0)@1, Bar(x: D(x: _))@3 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f1; +//│ f1 = function f(x, y) { +//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ let tmp; +//│ if (x === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new C1.class(y)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new Foo1.class(tmp1, 0)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(y)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new Bar1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ let m; +//│ m = tmp.x; +//│ if (m.__tag$ === 0) { +//│ return m.x + 1 +//│ } +//│ return runtime.Unit; +//│ } else if (tmp.__tag$ === 3) { +//│ let m; +//│ m = tmp.x; +//│ if (m.__tag$ === 2) { return m.x + 2 } +//│ return runtime.Unit; +//│ } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun f(x, y) = + let t = if x then new Foo(new C(y), 0) else new Bar(new D(y)) + @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => + let m = g(t) + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 + () => + let m = h(t) + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +private fun g(t) = t.Foo#x +private fun h(t) = t.Bar#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result, Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ class-tags collection-phase > track consumption at Foo.x@result in g +//│ class-tags collection-phase > track consumption at Bar.x@result in h +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, Foo@result, D@result, Bar@result +//│ class-tags web-computation-phase > field accesses: C.x@result, D.x@result, Foo.x@result, Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: _) +//│ class-tags transform-phase > allocated tag 1 for Foo(x: C(x: _), y: 0) +//│ class-tags transform-phase > allocated tag 2 for D(x: _) +//│ class-tags transform-phase > allocated tag 3 for Bar(x: D(x: _)) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: C(x: _), y: 0)@1, Bar(x: D(x: _))@3 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f2, g1, h1; +//│ f2 = function f(x, y) { +//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ let tmp; +//│ if (x === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new C1.class(y)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new Foo1.class(tmp1, 0)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(y)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new Bar1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ let inlinedVal; +//│ inlinedVal = tmp.x; +//│ if (inlinedVal.__tag$ === 0) { +//│ return inlinedVal.x + 1 +//│ } +//│ return runtime.Unit; +//│ } else if (tmp.__tag$ === 3) { +//│ let inlinedVal; +//│ inlinedVal = tmp.x; +//│ if (inlinedVal.__tag$ === 2) { +//│ return inlinedVal.x + 2 +//│ } +//│ return runtime.Unit; +//│ } +//│ return runtime.Unit; +//│ }; +//│ g1 = function g(t) { +//│ runtime.checkArgs("g", 1, true, arguments.length); +//│ return t.x +//│ }; +//│ h1 = function h(t) { +//│ runtime.checkArgs("h", 1, true, arguments.length); +//│ return t.x +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun f(x, y) = + let t = if x then new Foo(new C(y), 0) else new Bar(new D(y)) + @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => g(t) + () => h(t) +private fun g(t) = + let m = t.Foo#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +private fun h(t) = + let m = t.Bar#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result, Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ class-tags collection-phase > track consumption at Foo.x@result, match@result in g +//│ class-tags collection-phase > track consumption at match@result, Bar.x@result in h +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, Foo@result, D@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, C.x@result, Bar.x@result, D.x@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: _) +//│ class-tags transform-phase > allocated tag 1 for Foo(x: C(x: _), y: 0) +//│ class-tags transform-phase > allocated tag 2 for D(x: _) +//│ class-tags transform-phase > allocated tag 3 for Bar(x: D(x: _)) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: C(x: _), y: 0)@1, Bar(x: D(x: _))@3 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f3, g2, h2; +//│ f3 = function f(x, y) { +//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ let tmp; +//│ if (x === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new C1.class(y)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new Foo1.class(tmp1, 0)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(y)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new Bar1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ let m; +//│ m = tmp.x; +//│ if (m.__tag$ === 0) { +//│ return m.x + 1 +//│ } +//│ return runtime.Unit; +//│ } else if (tmp.__tag$ === 3) { +//│ let m; +//│ m = tmp.x; +//│ if (m.__tag$ === 2) { +//│ return m.x + 2 +//│ } +//│ return runtime.Unit; +//│ } +//│ return runtime.Unit; +//│ }; +//│ g2 = function g(t) { +//│ runtime.checkArgs("g", 1, true, arguments.length); +//│ let m, shapeMatchResult; +//│ m = t.x; +//│ if (m.__tag$ === 0) { +//│ shapeMatchResult = m.x + 1; +//│ } +//│ return shapeMatchResult +//│ }; +//│ h2 = function h(t) { +//│ runtime.checkArgs("h", 1, true, arguments.length); +//│ let m, shapeMatchResult; +//│ m = t.x; +//│ if (m.__tag$ === 2) { shapeMatchResult = m.x + 2; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:ssjs +fun f(x, y) = + let t = if x then new Foo(new C(y), 0) else new Bar(new D(y)) + let s1 = @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => g(t) + () => h(t) + let s2 = @matchShapes(Foo(_, _), Bar(_)) shape.match of t, + () => g(t) + () => h(t) + s1 + s2 +private fun g(t) = + let m = t.Foo#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +private fun h(t) = + let m = t.Bar#x + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result, Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ class-tags collection-phase > track consumption at Foo.x@result, match@result in g +//│ class-tags collection-phase > track consumption at match@result, Bar.x@result in h +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, Foo@result, D@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, C.x@result, Bar.x@result, D.x@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result, match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: _) +//│ class-tags transform-phase > allocated tag 1 for Foo(x: C(x: _), y: 0) +//│ class-tags transform-phase > allocated tag 2 for D(x: _) +//│ class-tags transform-phase > allocated tag 3 for Bar(x: D(x: _)) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: C(x: _), y: 0)@1, Bar(x: D(x: _))@3 +//│ class-tags transform-phase > match shapes Foo(x: _, y: _), Bar(x: _) against Foo(x: C(x: _), y: 0)@1, Bar(x: D(x: _))@3 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@0 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f4, g3, h3; +//│ f4 = function f(x, y) { +//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ let tmp, shapeMatchResult; +//│ if (x === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new C1.class(y)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new Foo1.class(tmp1, 0)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(y)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new Bar1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ let m, shapeMatchResult1; +//│ m = tmp.x; +//│ if (m.__tag$ === 0) { +//│ shapeMatchResult1 = m.x + 1; +//│ } +//│ shapeMatchResult = shapeMatchResult1; +//│ } else if (tmp.__tag$ === 3) { +//│ let m, shapeMatchResult1; +//│ m = tmp.x; +//│ if (m.__tag$ === 2) { +//│ shapeMatchResult1 = m.x + 2; +//│ } +//│ shapeMatchResult = shapeMatchResult1; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ let m, shapeMatchResult1; +//│ m = tmp.x; +//│ if (m.__tag$ === 0) { +//│ shapeMatchResult1 = m.x + 1; +//│ return shapeMatchResult + shapeMatchResult1 +//│ } +//│ return shapeMatchResult + undefined; +//│ } else if (tmp.__tag$ === 3) { +//│ let m, shapeMatchResult1; +//│ m = tmp.x; +//│ if (m.__tag$ === 2) { +//│ shapeMatchResult1 = m.x + 2; +//│ return shapeMatchResult + shapeMatchResult1 +//│ } +//│ return shapeMatchResult + undefined; +//│ } +//│ return shapeMatchResult + undefined; +//│ }; +//│ g3 = function g(t) { +//│ runtime.checkArgs("g", 1, true, arguments.length); +//│ let m, shapeMatchResult; +//│ m = t.x; +//│ if (m.__tag$ === 0) { +//│ shapeMatchResult = m.x + 1; +//│ } +//│ return shapeMatchResult +//│ }; +//│ h3 = function h(t) { +//│ runtime.checkArgs("h", 1, true, arguments.length); +//│ let m, shapeMatchResult; +//│ m = t.x; +//│ if (m.__tag$ === 2) { shapeMatchResult = m.x + 2; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +f(true, 0) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 2 + + +f(false, 0) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 4 + + +:ssjs +fun f(x, y) = + let t = if x then new Foo(y, 0) else new Bar(y) + @matchShapes(Foo(_, 0), Bar(_)) shape.match of t, + () => + let m = g(t.Foo#x) + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 + () => + let m = h(t.Bar#x) + @matchShapes(C(_), D(_)) shape.match of m, () => m.C#x + 1, () => m.D#x + 2 +private fun g(x) = new C(x) +private fun h(x) = new D(x) +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result, Foo@result in f +//│ class-tags collection-phase > track consumption at match@result in f +//│ class-tags collection-phase > track construction of C@result in g +//│ class-tags collection-phase > track construction of D@result in h +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Foo@result, Bar@result +//│ class-tags web-computation-phase > field accesses: Foo.x@result, Bar.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ class-tags web-computation-phase > web 1: +//│ class-tags web-computation-phase > producers: C@result +//│ class-tags web-computation-phase > field accesses: C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ class-tags web-computation-phase > web 2: +//│ class-tags web-computation-phase > producers: D@result +//│ class-tags web-computation-phase > field accesses: D.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Foo(x: _, y: 0) +//│ class-tags transform-phase > allocated tag 1 for Bar(x: _) +//│ class-tags transform-phase > allocated tag 2 for C(x: _) +//│ class-tags transform-phase > allocated tag 3 for D(x: _) +//│ class-tags transform-phase > match shapes Foo(x: _, y: 0), Bar(x: _) against Foo(x: _, y: 0)@0, Bar(x: _)@1 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@2 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@3 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against C(x: _)@2 +//│ class-tags transform-phase > match shapes C(x: _), D(x: _) against D(x: _)@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let f5, g4, h4; +//│ f5 = function f(x, y) { +//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ let tmp; +//│ if (x === true) { +//│ let tmp1; +//│ tmp1 = (new Foo1.class(y, 0)); +//│ tmp1.__tag$ = 0; +//│ tmp = tmp1; +//│ } else { +//│ let tmp1; +//│ tmp1 = (new Bar1.class(y)); +//│ tmp1.__tag$ = 1; +//│ tmp = tmp1; +//│ } +//│ if (tmp.__tag$ === 0) { +//│ let x1, tmp1; +//│ x1 = tmp.x; +//│ tmp1 = (new C1.class(x1)); +//│ tmp1.__tag$ = 2; +//│ if (tmp1.__tag$ === 2) { +//│ return tmp1.x + 1 +//│ } +//│ return runtime.Unit; +//│ } else if (tmp.__tag$ === 1) { +//│ let x1, tmp1; +//│ x1 = tmp.x; +//│ tmp1 = (new D1.class(x1)); +//│ tmp1.__tag$ = 3; +//│ if (tmp1.__tag$ === 3) { +//│ return tmp1.x + 2 +//│ } +//│ return runtime.Unit; +//│ } +//│ return runtime.Unit; +//│ }; +//│ g4 = function g(x) { +//│ runtime.checkArgs("g", 1, true, arguments.length); +//│ let tmp; +//│ tmp = (new C1.class(x)); +//│ tmp.__tag$ = 2; +//│ return tmp +//│ }; +//│ h4 = function h(x) { +//│ runtime.checkArgs("h", 1, true, arguments.length); +//│ let tmp; +//│ tmp = (new D1.class(x)); +//│ tmp.__tag$ = 3; +//│ return tmp +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +private fun foo(x) = + if x then new C(0) else new D(0) +fun bar(x, y) = + let t = foo(x) + let m = if y then new Foo(t, 0) else new Bar(t) + @matchShapes(Foo(C(0), 0), Bar(C(0)), Foo(D(0), 0), Bar(D(0))) shape.match of m, + () => 1 + () => 2 + () => 3 + () => 4 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Bar@result, Foo@result in bar +//│ class-tags collection-phase > track consumption at match@result in bar +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: C@result, D@result, Foo@result, Bar@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for C(x: 0) +//│ class-tags transform-phase > allocated tag 1 for D(x: 0) +//│ class-tags transform-phase > allocated tag 2 for Foo(x: C(x: 0), y: 0) +//│ class-tags transform-phase > allocated tag 3 for Foo(x: D(x: 0), y: 0) +//│ class-tags transform-phase > allocated tag 4 for Bar(x: C(x: 0)) +//│ class-tags transform-phase > allocated tag 5 for Bar(x: D(x: 0)) +//│ class-tags transform-phase > match shapes Foo(x: C(x: 0), y: 0), Bar(x: C(x: 0)), Foo(x: D(x: 0), y: 0), Bar(x: D(x: 0)) against Foo(x: C(x: 0), y: 0)@2, Foo(x: D(x: 0), y: 0)@3, Bar(x: C(x: 0))@4, Bar(x: D(x: 0))@5 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let bar, foo; +//│ foo = function foo(x) { +//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ if (x === true) { +//│ let tmp; +//│ tmp = (new C1.class(0)); +//│ tmp.__tag$ = 0; +//│ return tmp +//│ } +//│ { +//│ let tmp; +//│ tmp = (new D1.class(0)); +//│ tmp.__tag$ = 1; +//│ return tmp; +//│ } +//│ }; +//│ bar = function bar(x, y) { +//│ runtime.checkArgs("bar", 2, true, arguments.length); +//│ let tmp, inlinedVal; +//│ if (x === true) { +//│ let tmp1; +//│ tmp1 = (new C1.class(0)); +//│ tmp1.__tag$ = 0; +//│ inlinedVal = tmp1; +//│ } else { +//│ let tmp1; +//│ tmp1 = (new D1.class(0)); +//│ tmp1.__tag$ = 1; +//│ inlinedVal = tmp1; +//│ } +//│ if (y === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new Foo1.class(inlinedVal, 0)); +//│ tmp2 = inlinedVal.__tag$ === 0; +//│ if (tmp2 === true) { +//│ tmp1.__tag$ = 2; +//│ } else { +//│ let tmp3; +//│ tmp3 = inlinedVal.__tag$ === 1; +//│ if (tmp3 === true) { +//│ tmp1.__tag$ = 3; +//│ } +//│ } +//│ tmp = tmp1; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new Bar1.class(inlinedVal)); +//│ tmp2 = inlinedVal.__tag$ === 0; +//│ if (tmp2 === true) { +//│ tmp1.__tag$ = 4; +//│ } else { +//│ let tmp3; +//│ tmp3 = inlinedVal.__tag$ === 1; +//│ if (tmp3 === true) { +//│ tmp1.__tag$ = 5; +//│ } +//│ } +//│ tmp = tmp1; +//│ } +//│ if (tmp.__tag$ === 2) { +//│ return 1 +//│ } else if (tmp.__tag$ === 4) { +//│ return 2 +//│ } else if (tmp.__tag$ === 3) { +//│ return 3 +//│ } else if (tmp.__tag$ === 5) { return 4 } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class Box(val x) +class A(val x) +class B(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ssjs +private fun inspect(value) = + @matchShapes(Box(_)) shape.match of value, () => 0 +fun first() = + inspect(new Box(0)) + let value = new A(0) + @matchShapes(A(_)) shape.match of value, () => 1 +fun second() = + inspect(new Box(0)) + let value = new B(0) + @matchShapes(B(_)) shape.match of value, () => 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track consumption at match@result in inspect +//│ class-tags collection-phase > track construction of A@result, Box@result in first +//│ class-tags collection-phase > track consumption at match@result in first +//│ class-tags collection-phase > track construction of B@result, Box@result in second +//│ class-tags collection-phase > track consumption at match@result in second +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result, A@result, Box@result, B@result +//│ class-tags web-computation-phase > pattern matches: match@result, match@result, match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: 0) +//│ class-tags transform-phase > allocated tag 1 for A(x: 0) +//│ class-tags transform-phase > allocated tag 2 for B(x: 0) +//│ class-tags transform-phase > match shapes Box(x: _) against Box(x: 0)@0 +//│ class-tags transform-phase > match shapes A(x: _) against A(x: 0)@1 +//│ class-tags transform-phase > match shapes B(x: _) against B(x: 0)@2 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let inspect, first, second; +//│ inspect = function inspect(value) { +//│ runtime.checkArgs("inspect", 1, true, arguments.length); +//│ if (value.__tag$ === 0) { +//│ return 0 +//│ } +//│ return 0; +//│ }; +//│ first = function first() { +//│ runtime.checkArgs("first", 0, true, arguments.length); +//│ let tmp, tmp1; +//│ tmp = (new Box1.class(0)); +//│ tmp.__tag$ = 0; +//│ tmp1 = (new A1.class(0)); +//│ tmp1.__tag$ = 1; +//│ if (tmp1.__tag$ === 1) { +//│ return 1 +//│ } +//│ return 1; +//│ }; +//│ second = function second() { +//│ runtime.checkArgs("second", 0, true, arguments.length); +//│ let tmp, tmp1; +//│ tmp = (new Box1.class(0)); +//│ tmp.__tag$ = 0; +//│ tmp1 = (new B1.class(0)); +//│ tmp1.__tag$ = 2; +//│ if (tmp1.__tag$ === 2) { return 2 } +//│ return 2; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Nested.mls b/hkmc2/shared/src/test/mlscript/class-tags/Nested.mls new file mode 100644 index 0000000000..6877be7900 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Nested.mls @@ -0,0 +1,358 @@ +:classTags debug mono +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +class Cons(val x, val xs) +object Nil +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +fun foo(x, y, z) = + let ls = new Cons(1, new Cons(2, new Cons(3, new Cons(x, new Cons(y, new Cons(z, Nil)))))) + bar(ls) +private fun bar(ls) = + @matchShapes(Cons(1, Cons(2, Cons(3, Cons(_, Cons(_, Cons(_, Nil))))))) + shape.match of ls, () => ls.Cons#xs.Cons#xs.Cons#xs.Cons#x + ls.Cons#xs.Cons#xs.Cons#xs.Cons#xs.Cons#x + + ls.Cons#xs.Cons#xs.Cons#xs.Cons#xs.Cons#xs.Cons#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Cons@result in foo +//│ class-tags collection-phase > track consumption at match@result in bar +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Nil@Nil, Cons@result, Cons@result, Cons@result, Cons@result, Cons@result, Cons@result +//│ class-tags web-computation-phase > field accesses: Cons.x@result, Cons.xs@result, Cons.xs@result, Cons.xs@result, Cons.x@result, Cons.xs@result, Cons.xs@result, Cons.xs@result, Cons.xs@result, Cons.x@result, Cons.xs@result, Cons.xs@result, Cons.xs@result, Cons.xs@result, Cons.xs@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Nil +//│ class-tags transform-phase > allocated tag 1 for Cons(x: _, xs: Nil) +//│ class-tags transform-phase > allocated tag 2 for Cons(x: _, xs: Cons(x: _, xs: Nil)) +//│ class-tags transform-phase > allocated tag 3 for Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil))) +//│ class-tags transform-phase > allocated tag 4 for Cons(x: 3, xs: Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil)))) +//│ class-tags transform-phase > allocated tag 5 for Cons(x: 2, xs: Cons(x: 3, xs: Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil))))) +//│ class-tags transform-phase > allocated tag 6 for Cons(x: 1, xs: Cons(x: 2, xs: Cons(x: 3, xs: Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil)))))) +//│ class-tags transform-phase > match shapes Cons(x: 1, xs: Cons(x: 2, xs: Cons(x: 3, xs: Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil)))))) against Cons(x: 1, xs: Cons(x: 2, xs: Cons(x: 3, xs: Cons(x: _, xs: Cons(x: _, xs: Cons(x: _, xs: Nil))))))@6 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let bar, foo; +//│ foo = function foo(x, y, z) { +//│ runtime.checkArgs("foo", 3, true, arguments.length); +//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5; +//│ tmp = (new Cons1.class(z, Nil1)); +//│ tmp.__tag$ = 1; +//│ tmp1 = (new Cons1.class(y, tmp)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new Cons1.class(x, tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp3 = (new Cons1.class(3, tmp2)); +//│ tmp3.__tag$ = 4; +//│ tmp4 = (new Cons1.class(2, tmp3)); +//│ tmp4.__tag$ = 5; +//│ tmp5 = (new Cons1.class(1, tmp4)); +//│ tmp5.__tag$ = 6; +//│ if (tmp5.__tag$ === 6) { +//│ let tmp6, tmp7; +//│ tmp6 = tmp5.xs.xs.xs.x + tmp5.xs.xs.xs.xs.x; +//│ tmp7 = + tmp5.xs.xs.xs.xs.xs.x; +//│ return tmp6 + tmp7 +//│ } +//│ return runtime.Unit; +//│ }; +//│ bar = function bar(ls) { +//│ runtime.checkArgs("bar", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (ls.__tag$ === 6) { +//│ let tmp, tmp1; +//│ tmp = ls.xs.xs.xs.x + ls.xs.xs.xs.xs.x; +//│ tmp1 = + ls.xs.xs.xs.xs.xs.x; +//│ shapeMatchResult = tmp + tmp1; +//│ } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class C(val x) +class D(val x) +class E(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ssjs +fun foo(x, y) = + let t = if y then new C(new D(x)) else new C(new E(1)) + foofoo(t) +private fun foofoo(t) = + @matchShapes(C(D(_)), C(E(1))) shape.match of t, + () => t.C#x.D#x + 1 + () => 0 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result in foo +//│ class-tags collection-phase > track consumption at match@result in foofoo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: D@result, C@result, E@result, C@result +//│ class-tags web-computation-phase > field accesses: D.x@result, C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for D(x: _) +//│ class-tags transform-phase > allocated tag 1 for C(x: D(x: _)) +//│ class-tags transform-phase > allocated tag 2 for E(x: 1) +//│ class-tags transform-phase > allocated tag 3 for C(x: E(x: 1)) +//│ class-tags transform-phase > match shapes C(x: D(x: _)), C(x: E(x: 1)) against C(x: D(x: _))@1, C(x: E(x: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo1, foofoo; +//│ foo1 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp; +//│ if (y === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(x)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new E1.class(1)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ return tmp.x.x + 1 +//│ } else if (tmp.__tag$ === 3) { +//│ return 0 +//│ } +//│ return runtime.Unit; +//│ }; +//│ foofoo = function foofoo(t) { +//│ runtime.checkArgs("foofoo", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (t.__tag$ === 1) { +//│ shapeMatchResult = t.x.x + 1; +//│ } else if (t.__tag$ === 3) { shapeMatchResult = 0; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:ssjs +fun foo(x, y) = + let t = new C(if y then new D(x) else new E(1)) + foofoo(t) +private fun foofoo(t) = + @matchShapes(C(D(_)), C(E(1))) shape.match of t, + () => t.C#x.D#x + 1 + () => 0 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, D@result in foo +//│ class-tags collection-phase > track consumption at match@result in foofoo +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: D@result, E@result, C@result +//│ class-tags web-computation-phase > field accesses: D.x@result, C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for D(x: _) +//│ class-tags transform-phase > allocated tag 1 for E(x: 1) +//│ class-tags transform-phase > allocated tag 2 for C(x: D(x: _)) +//│ class-tags transform-phase > allocated tag 3 for C(x: E(x: 1)) +//│ class-tags transform-phase > match shapes C(x: D(x: _)), C(x: E(x: 1)) against C(x: D(x: _))@2, C(x: E(x: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo2, foofoo1; +//│ foo2 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp, tmp1, tmp2; +//│ if (y === true) { +//│ let tmp3; +//│ tmp3 = (new D1.class(x)); +//│ tmp3.__tag$ = 0; +//│ tmp = tmp3; +//│ } else { +//│ let tmp3; +//│ tmp3 = (new E1.class(1)); +//│ tmp3.__tag$ = 1; +//│ tmp = tmp3; +//│ } +//│ tmp1 = (new C1.class(tmp)); +//│ tmp2 = tmp.__tag$ === 0; +//│ if (tmp2 === true) { +//│ tmp1.__tag$ = 2; +//│ } else { +//│ let tmp3; +//│ tmp3 = tmp.__tag$ === 1; +//│ if (tmp3 === true) { +//│ tmp1.__tag$ = 3; +//│ } +//│ } +//│ if (tmp1.__tag$ === 2) { +//│ return tmp1.x.x + 1 +//│ } else if (tmp1.__tag$ === 3) { +//│ return 0 +//│ } +//│ return runtime.Unit; +//│ }; +//│ foofoo1 = function foofoo(t) { +//│ runtime.checkArgs("foofoo", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (t.__tag$ === 2) { +//│ shapeMatchResult = t.x.x + 1; +//│ } else if (t.__tag$ === 3) { shapeMatchResult = 0; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class Pair(val x, val y) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +fun pair(x, y) = + let p = new Pair( + if x then new D(1) else new E(2), + if y then new D(3) else new E(4)) + @matchShapes(Pair(D(_), D(_)), Pair(D(_), E(_)), Pair(E(_), D(_)), Pair(E(_), E(_))) + shape.match of p, + () => 0 + () => 1 + () => 2 + () => 3 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Pair@result in pair +//│ class-tags collection-phase > track consumption at match@result in pair +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: D@result, E@result, D@result, E@result, Pair@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for D(x: 1) +//│ class-tags transform-phase > allocated tag 1 for E(x: 2) +//│ class-tags transform-phase > allocated tag 2 for D(x: 3) +//│ class-tags transform-phase > allocated tag 3 for E(x: 4) +//│ class-tags transform-phase > allocated tag 4 for Pair(x: D(x: 1), y: D(x: 3)) +//│ class-tags transform-phase > allocated tag 5 for Pair(x: D(x: 1), y: E(x: 4)) +//│ class-tags transform-phase > allocated tag 6 for Pair(x: E(x: 2), y: D(x: 3)) +//│ class-tags transform-phase > allocated tag 7 for Pair(x: E(x: 2), y: E(x: 4)) +//│ class-tags transform-phase > match shapes Pair(x: D(x: _), y: D(x: _)), Pair(x: D(x: _), y: E(x: _)), Pair(x: E(x: _), y: D(x: _)), Pair(x: E(x: _), y: E(x: _)) against Pair(x: D(x: 1), y: D(x: 3))@4, Pair(x: D(x: 1), y: E(x: 4))@5, Pair(x: E(x: 2), y: D(x: 3))@6, Pair(x: E(x: 2), y: E(x: 4))@7 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let pair; +//│ pair = function pair(x, y) { +//│ runtime.checkArgs("pair", 2, true, arguments.length); +//│ let tmp, tmp1, tmp2, tmp3, tmp4; +//│ if (x === true) { +//│ let tmp5; +//│ tmp5 = (new D1.class(1)); +//│ tmp5.__tag$ = 0; +//│ tmp = tmp5; +//│ } else { +//│ let tmp5; +//│ tmp5 = (new E1.class(2)); +//│ tmp5.__tag$ = 1; +//│ tmp = tmp5; +//│ } +//│ if (y === true) { +//│ let tmp5; +//│ tmp5 = (new D1.class(3)); +//│ tmp5.__tag$ = 2; +//│ tmp1 = tmp5; +//│ } else { +//│ let tmp5; +//│ tmp5 = (new E1.class(4)); +//│ tmp5.__tag$ = 3; +//│ tmp1 = tmp5; +//│ } +//│ tmp2 = (new Pair1.class(tmp, tmp1)); +//│ tmp3 = tmp.__tag$ === 0; +//│ if (tmp3 === true) { +//│ let tmp5; +//│ tmp5 = tmp1.__tag$ === 2; +//│ tmp4 = tmp5; +//│ } else { +//│ tmp4 = false; +//│ } +//│ if (tmp4 === true) { +//│ tmp2.__tag$ = 4; +//│ } else { +//│ let tmp5, tmp6; +//│ tmp5 = tmp.__tag$ === 0; +//│ if (tmp5 === true) { +//│ let tmp7; +//│ tmp7 = tmp1.__tag$ === 3; +//│ tmp6 = tmp7; +//│ } else { +//│ tmp6 = false; +//│ } +//│ if (tmp6 === true) { +//│ tmp2.__tag$ = 5; +//│ } else { +//│ let tmp7, tmp8; +//│ tmp7 = tmp.__tag$ === 1; +//│ if (tmp7 === true) { +//│ let tmp9; +//│ tmp9 = tmp1.__tag$ === 2; +//│ tmp8 = tmp9; +//│ } else { +//│ tmp8 = false; +//│ } +//│ if (tmp8 === true) { +//│ tmp2.__tag$ = 6; +//│ } else { +//│ let tmp9, tmp10; +//│ tmp9 = tmp.__tag$ === 1; +//│ if (tmp9 === true) { +//│ let tmp11; +//│ tmp11 = tmp1.__tag$ === 3; +//│ tmp10 = tmp11; +//│ } else { +//│ tmp10 = false; +//│ } +//│ if (tmp10 === true) { +//│ tmp2.__tag$ = 7; +//│ } +//│ } +//│ } +//│ } +//│ if (tmp2.__tag$ === 4) { +//│ return 0 +//│ } else if (tmp2.__tag$ === 5) { +//│ return 1 +//│ } else if (tmp2.__tag$ === 6) { +//│ return 2 +//│ } else if (tmp2.__tag$ === 7) { return 3 } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +:expect 6 +pair(true, true) + pair(true, false) + pair(false, true) + pair(false, false) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 6 diff --git a/hkmc2/shared/src/test/mlscript/class-tags/NoFreeze.mls b/hkmc2/shared/src/test/mlscript/class-tags/NoFreeze.mls new file mode 100644 index 0000000000..bf148401bb --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/NoFreeze.mls @@ -0,0 +1,18 @@ +:classTags debug mono +:ge +1 +//│ ═══[COMPILATION ERROR] Class tag insertion requires :noFreeze. + +//│ Error: hkmc2.ErrorReport: Class tag insertion requires :noFreeze. + +:ge +class Foo(val x, val y) +fun foo(x, y) = + let f = new Foo(x, y) + @matchShapes(Foo(_, _)) shape.match of f, () => f.Foo#x + f.Foo#y +//│ ═══[COMPILATION ERROR] Class tag insertion requires :noFreeze. + + +:ge +foo(1, 5) +//│ ═══[COMPILATION ERROR] Class tag insertion requires :noFreeze. diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Polymorphism.mls b/hkmc2/shared/src/test/mlscript/class-tags/Polymorphism.mls new file mode 100644 index 0000000000..7c96bbd1ca --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Polymorphism.mls @@ -0,0 +1,87 @@ +:classTags debug +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +class Box(val x) +class A(val x) +class B(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +private fun wrap(x) = new Box(x) +fun poly(flag) = + let a = wrap(new A(0)) + let b = wrap(new B(0)) + let value = if flag then a else b + @matchShapes(Box(A(_)), Box(B(_))) shape.match of value, () => 1, () => 2 +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of Box@result in wrap +//│ class-tags collection-phase > track consumption at match@result in poly +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: Box@result, Box@result, A@result, B@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for Box(x: A(x: 0)) +//│ class-tags transform-phase > allocated tag 1 for Box(x: B(x: 0)) +//│ class-tags transform-phase > allocated tag 2 for A(x: 0) +//│ class-tags transform-phase > allocated tag 3 for B(x: 0) +//│ class-tags transform-phase > match shapes Box(x: A(x: _)), Box(x: B(x: _)) against Box(x: A(x: 0))@0, Box(x: B(x: 0))@1 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let poly, wrap; +//│ wrap = function wrap(x) { +//│ runtime.checkArgs("wrap", 1, true, arguments.length); +//│ let tmp, tmp1; +//│ tmp = (new Box1.class(x)); +//│ tmp1 = x.__tag$ === 2; +//│ if (tmp1 === true) { +//│ tmp.__tag$ = 0; +//│ return tmp +//│ } +//│ { +//│ let tmp2; +//│ tmp2 = x.__tag$ === 3; +//│ if (tmp2 === true) { +//│ tmp.__tag$ = 1; +//│ return tmp +//│ } +//│ return tmp; +//│ } +//│ }; +//│ poly = function poly(flag) { +//│ runtime.checkArgs("poly", 1, true, arguments.length); +//│ let a, b, tmp, tmp1, tmp2; +//│ tmp1 = (new A1.class(0)); +//│ tmp1.__tag$ = 2; +//│ a = runtime.checkCall(wrap(tmp1)); +//│ tmp2 = (new B1.class(0)); +//│ tmp2.__tag$ = 3; +//│ b = runtime.checkCall(wrap(tmp2)); +//│ if (flag === true) { +//│ tmp = a; +//│ } else { +//│ tmp = b; +//│ } +//│ if (tmp.__tag$ === 0) { return 1 } else if (tmp.__tag$ === 1) { return 2 } +//│ return runtime.Unit; +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:expect 1 +poly(false) - poly(true) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase +//│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/class-tags/ReviewFlattenBudget.mls b/hkmc2/shared/src/test/mlscript/class-tags/ReviewFlattenBudget.mls new file mode 100644 index 0000000000..d5f3906a49 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/ReviewFlattenBudget.mls @@ -0,0 +1,22 @@ +// PR #555 follow-up: the 256-shape budget is checked after materializing the product. +// This 21-field input attempts to allocate 2^21 shapes before reporting a diagnostic. +// Kept behind :exit because it exhausts the test JVM instead of completing a golden snapshot. +// Reproduced at e785faeb3 in a separate JVM with -Xmx512m -XX:+ExitOnOutOfMemoryError: +// Terminating due to java.lang.OutOfMemoryError: Java heap space (exit 3, 13.46 seconds). +// To rerun, remove :exit and its separator, then run only ReviewFlattenBudget in a disposable, memory-limited JVM. + +:exit +==================================================================================================== + +:classTags mono +:noFreeze + +class A(val x) +class B(val x) +class Wide(val f0, val f1, val f2, val f3, val f4, val f5, val f6, val f7, val f8, val f9, val f10, val f11, val f12, val f13, val f14, val f15, val f16, val f17, val f18, val f19, val f20) + +:ge +fun flattenBudget(flag) = + let field = if flag then new A(0) else new B(0) + let value = new Wide(field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field, field) + @matchShapes(Wide(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _)) shape.match of value, () => 1 diff --git a/hkmc2/shared/src/test/mlscript/class-tags/ReviewFollowup.mls b/hkmc2/shared/src/test/mlscript/class-tags/ReviewFollowup.mls new file mode 100644 index 0000000000..21ce21cd65 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/ReviewFollowup.mls @@ -0,0 +1,70 @@ +:classTags mono +:js +:noFreeze + +class Box(val x) +class A(val x) +class B(val x) + + +// A safe outer allocation pulls an unsafe mixed local/external consumer back into the web. +fun unknownThroughField(flag, external) = + let local = new A(0) + let holder = new Box(local) + let _ = @matchShapes(Box(_)) shape.match of holder, () => 0 + let value = if flag then local else external + @matchShapes(A(_), B(_)) shape.match of value, () => 1, () => 2 + +:fixme +:expect 2 +unknownThroughField(false, new B(0)) +//│ ═══[RUNTIME ERROR] Expected: '2', got: '1' +//│ = 1 + + +// Immutable parameter declarations do not prevent a constructor from writing a dynamic field. +class Rewritten(val x) with + set this.("x") = new B(0) +fun rewrittenConstructor() = + let value = new Rewritten(new A(0)) + @matchShapes(Rewritten(A(_)), Rewritten(B(_))) shape.match of value, () => 1, () => 2 + +:fixme +:expect 2 +rewrittenConstructor() +//│ ═══[RUNTIME ERROR] Expected: '2', got: '1' +//│ = 1 + + +// Every shape.match call must be checked, including calls with no branches. +:breakme +:ge +fun zeroBranches() = + let value = new Box(0) + @matchShapes(Box(_)) shape.match of value + +:fixme +zeroBranches() +//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'match') + + +fun returnedThunk() = + let value = new Box(0) + let result = @matchShapes(Box(_)) shape.match of value, () => () => 42 + result() + +:expect 42 +returnedThunk() +//│ = 42 + + +// Anonymous branches preserve an ordinary recursive call without expanding it indefinitely. +fun recursiveLambdaBranch(n) = + if n > 0 then + let value = new Box(0) + @matchShapes(Box(_)) shape.match of value, () => recursiveLambdaBranch(n - 1) + else 42 + +:expect 42 +recursiveLambdaBranch(2) +//│ = 42 diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Subsumption.mls b/hkmc2/shared/src/test/mlscript/class-tags/Subsumption.mls new file mode 100644 index 0000000000..9522a63aff --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Subsumption.mls @@ -0,0 +1,216 @@ +:classTags debug mono +:js +:noFreeze + +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +class C(val x) +class D(val x) +class E(val x) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + +:ssjs +fun foo(x, y) = + let t = new C(if y then new D(x) else new E(1)) + inspect(t) +private fun inspect(t) = + @matchShapes(C(_)) shape.match of t, + () => t.C#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result in foo +//│ class-tags collection-phase > track consumption at match@result in inspect +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: D@result, E@result, C@result +//│ class-tags web-computation-phase > field accesses: C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for D(x: _) +//│ class-tags transform-phase > allocated tag 1 for E(x: 1) +//│ class-tags transform-phase > allocated tag 2 for C(x: D(x: _)) +//│ class-tags transform-phase > allocated tag 3 for C(x: E(x: 1)) +//│ class-tags transform-phase > match shapes C(x: _) against C(x: D(x: _))@2, C(x: E(x: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo, inspect; +//│ foo = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp, tmp1, tmp2; +//│ if (y === true) { +//│ let tmp3; +//│ tmp3 = (new D1.class(x)); +//│ tmp3.__tag$ = 0; +//│ tmp = tmp3; +//│ } else { +//│ let tmp3; +//│ tmp3 = (new E1.class(1)); +//│ tmp3.__tag$ = 1; +//│ tmp = tmp3; +//│ } +//│ tmp1 = (new C1.class(tmp)); +//│ tmp2 = tmp.__tag$ === 0; +//│ if (tmp2 === true) { +//│ tmp1.__tag$ = 2; +//│ } else { +//│ let tmp3; +//│ tmp3 = tmp.__tag$ === 1; +//│ if (tmp3 === true) { +//│ tmp1.__tag$ = 3; +//│ } +//│ } +//│ if (tmp1.__tag$ === 2) { +//│ return tmp1.x +//│ } else if (tmp1.__tag$ === 3) { +//│ return tmp1.x +//│ } +//│ return runtime.Unit; +//│ }; +//│ inspect = function inspect(t) { +//│ runtime.checkArgs("inspect", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (t.__tag$ === 2) { +//│ shapeMatchResult = t.x; +//│ } else if (t.__tag$ === 3) { shapeMatchResult = t.x; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:ssjs +fun foo(x, y) = + let t = if y then new C(new D(x)) else new C(new E(1)) + inspect(t) +private fun inspect(t) = + @matchShapes(C(_)) shape.match of t, + () => t.C#x +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result in foo +//│ class-tags collection-phase > track consumption at match@result in inspect +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: D@result, C@result, E@result, C@result +//│ class-tags web-computation-phase > field accesses: C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for D(x: _) +//│ class-tags transform-phase > allocated tag 1 for C(x: D(x: _)) +//│ class-tags transform-phase > allocated tag 2 for E(x: 1) +//│ class-tags transform-phase > allocated tag 3 for C(x: E(x: 1)) +//│ class-tags transform-phase > match shapes C(x: _) against C(x: D(x: _))@1, C(x: E(x: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo1, inspect1; +//│ foo1 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp; +//│ if (y === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new D1.class(x)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new E1.class(1)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 1) { +//│ return tmp.x +//│ } else if (tmp.__tag$ === 3) { +//│ return tmp.x +//│ } +//│ return runtime.Unit; +//│ }; +//│ inspect1 = function inspect(t) { +//│ runtime.checkArgs("inspect", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (t.__tag$ === 1) { +//│ shapeMatchResult = t.x; +//│ } else if (t.__tag$ === 3) { shapeMatchResult = t.x; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +class F(val y) extends E(0) +//│ >>> start class-tags collection-phase +//│ <<< end class-tags collection-phase +//│ >>> start class-tags transform-phase +//│ <<< end class-tags transform-phase + + +:ssjs +fun foo(x, y) = + let t = if y then new C(new E(x)) else new C(new F(1)) + inspect(t) +private fun inspect(t) = + @matchShapes(C(F(_)), C(E(_))) shape.match of t, + () => t.C#x.E#x + () => t.C#x.F#y +//│ >>> start class-tags collection-phase +//│ class-tags collection-phase > track construction of C@result, E@result, F@result in foo +//│ class-tags collection-phase > track consumption at match@result in inspect +//│ <<< end class-tags collection-phase +//│ >>> start class-tags web-computation-phase +//│ class-tags web-computation-phase > web 0: +//│ class-tags web-computation-phase > producers: E@result, C@result, F@result, C@result +//│ class-tags web-computation-phase > field accesses: E.x@result, C.x@result, F.y@result, C.x@result +//│ class-tags web-computation-phase > pattern matches: match@result +//│ <<< end class-tags web-computation-phase +//│ >>> start class-tags transform-phase +//│ class-tags transform-phase > allocated tag 0 for E(x: _) +//│ class-tags transform-phase > allocated tag 1 for C(x: E(x: _)) +//│ class-tags transform-phase > allocated tag 2 for F(y: 1) +//│ class-tags transform-phase > allocated tag 3 for C(x: F(y: 1)) +//│ class-tags transform-phase > match shapes C(x: F(y: _)), C(x: E(x: _)) against C(x: E(x: _))@1, C(x: F(y: 1))@3 +//│ <<< end class-tags transform-phase +//│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— +//│ let foo2, inspect2; +//│ foo2 = function foo(x, y) { +//│ runtime.checkArgs("foo", 2, true, arguments.length); +//│ let tmp; +//│ if (y === true) { +//│ let tmp1, tmp2; +//│ tmp1 = (new E1.class(x)); +//│ tmp1.__tag$ = 0; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 1; +//│ tmp = tmp2; +//│ } else { +//│ let tmp1, tmp2; +//│ tmp1 = (new F1.class(1)); +//│ tmp1.__tag$ = 2; +//│ tmp2 = (new C1.class(tmp1)); +//│ tmp2.__tag$ = 3; +//│ tmp = tmp2; +//│ } +//│ if (tmp.__tag$ === 3) { +//│ return tmp.x.x +//│ } else if (tmp.__tag$ === 1) { +//│ return tmp.x.y +//│ } +//│ return runtime.Unit; +//│ }; +//│ inspect2 = function inspect(t) { +//│ runtime.checkArgs("inspect", 1, true, arguments.length); +//│ let shapeMatchResult; +//│ if (t.__tag$ === 3) { +//│ shapeMatchResult = t.x.x; +//│ } else if (t.__tag$ === 1) { shapeMatchResult = t.x.y; } +//│ return shapeMatchResult +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/class-tags/Todos.mls b/hkmc2/shared/src/test/mlscript/class-tags/Todos.mls new file mode 100644 index 0000000000..3e0ee37463 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/class-tags/Todos.mls @@ -0,0 +1,22 @@ + +:global +:classTags mono +:noFreeze +:todo + + +// TODO: make the tag a real field +:wasm +class Box(val x) +private fun tagged() = + let value = new Box(0) + @matchShapes(Box(_)) shape.match of value, () => 1 +tagged() +//│ ═══[COMPILATION ERROR] WatBuilder::returningTerm for AssignField(...) without a resolved symbol is not implemented (field `__tag$`). Use `_.[_]` for index-based accesses. +//│ ╔══[COMPILATION ERROR] WatBuilder::result for field selection without a resolved symbol is not implemented (field `__tag$`). Use `_.[_]` for index-based accesses. +//│ ║ l.13: @matchShapes(Box(_)) shape.match of value, () => 1 +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] WatBuilder::returningTerm for AssignField(...) without a resolved symbol is not implemented (field `__tag$`). Use `_.[_]` for index-based accesses. +//│ ╔══[COMPILATION ERROR] WatBuilder::result for field selection without a resolved symbol is not implemented (field `__tag$`). Use `_.[_]` for index-based accesses. +//│ ║ l.13: @matchShapes(Box(_)) shape.match of value, () => 1 +//│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls index 9ce9ecc860..56bafc9a78 100644 --- a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls +++ b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls @@ -366,6 +366,7 @@ declare module annotations with object compile object buffered object bufferable + object matchShapes object generator object async @@ -375,6 +376,9 @@ declare module handlers with declare module scope with fun locally +declare module shape with + fun match + declare module runtime with fun suspend diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls index eb54d2013c..dbefb62376 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls @@ -178,6 +178,7 @@ declare module annotations with object compile object buffered object bufferable + object matchShapes object generator object async @@ -187,6 +188,9 @@ declare module handlers with declare module scope with fun locally +declare module shape with + fun match + declare module runtime with fun suspend diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index a7cb944188..e07e6b952b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -97,6 +97,7 @@ abstract class MLsDiffMaker extends DiffMaker: val inlineThreshold = Command("inlineThreshold")(_.trim.toInt) val noTailRecOpt = NullaryCommand("noTailRec") val deforest = Command("deforest")(_.trim) + val classTags = Command("classTags")(_.trim) val patMatConsequentSharingThreshold = Command("patMatConsequentSharingThreshold")(_.trim.toInt) val flowBasedOpt = Command("flowBasedOpt")(_.trim) @@ -111,6 +112,7 @@ abstract class MLsDiffMaker extends DiffMaker: "logAccumulator", "noLogAccumulator", ) + private val ClassTagsKnownFlags = Set("debug", "mono") private val FlowBasedOptKnownFlags = Set("debug", "debugEta", "debugDpe", "debugDce", "on", "off", "mono", "poly") @@ -172,6 +174,13 @@ abstract class MLsDiffMaker extends DiffMaker: target = if wasm.isSet then CompilationTarget.Wasm else CompilationTarget.JS, rewriteWhileLoops = rewriteWhile.isSet, + classTags = Opt.when(classTags.isSet): + val flags = parseFlags(classTags.get) + reportUnknownFlags(":classTags", flags, ClassTagsKnownFlags) + ClassTags( + debug = flags.contains("debug"), + mono = flags.contains("mono"), + ), qqEnabled = importQQ.isSet, funcToCls = funcToCls.isSet, commentGeneratedCode = debug.isSet,