diff --git a/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala new file mode 100644 index 0000000000..7e479e5586 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala @@ -0,0 +1,82 @@ +package hkmc2 +package codegen + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.util.boundary +import sourcecode.{ Line, FileName, Name } + +import hkmc2.utils.*, shorthands.* +import hkmc2.utils.* +import hkmc2.utils.SymbolSubst +import hkmc2.Message.MessageContext + +import syntax.{Literal, Tree} +import semantics.* +import semantics.Elaborator.ctx +import semantics.Elaborator.State +import hkmc2.Config.EffectHandlers +import scala.collection.mutable.ArrayBuffer + + +class AsyncLowering(using TL, Raise, Elaborator.State, Elaborator.Ctx, Config): + + object Rewriter extends BlockTransformer(SymbolSubst.Id): + val collectedFunDefn: ArrayBuffer[FunDefn] = new ArrayBuffer + var inAsyncFun: Bool = false + + inline def wrapAwait[T](innerAllowAwait: Bool)(inline thunk: => T): T = + val saved = inAsyncFun + inAsyncFun = innerAllowAwait + val result = thunk + inAsyncFun = saved + result + + override def applyFunDefn(fun: FunDefn): FunDefn = + if !fun.async then return wrapAwait(false)(super.applyFunDefn(fun)) + val outerBms = BlockMemberSymbol(fun.sym.nme, Nil, fun.sym.nameIsMeaningful) + val outerDsym = TermSymbol(syntax.Fun, N, fun.dSym.id) + val outerParams = fun.params.flatMap: pl => + pl.allParams.map: p => + val v = p.sym + val nv = VarSymbol(v.id) + (p, p.copy(sym = nv)) + val symMap = outerParams.iterator.map(p => p._1.sym -> p._2.sym).toMap[SimpleSymbol, SimpleSymbol] + val thisVar = VarSymbol(Tree.Ident("this")) + val thisParam = fun.owner.map(_ => Param.simple(thisVar)) + val vars = fun.params.flatMap(_.paramSyms) + val noAsync = fun.annotations.filterNot(_ is Annot.Async) + val transformer = new BlockTransformer(SymbolSubst.Id): + override def applySimpleSymbol(sym: SimpleSymbol): SimpleSymbol = + symMap.getOrElse(sym, sym) + override def applyValue(v: Value)(k: Value => Block): Block = v match + case Value.This(sym) if fun.owner.contains(sym) => + k(Value.SimpleRef(thisVar)) + case _ => super.applyValue(v)(k) + val newBody = transformer.applyBlock(wrapAwait(true)(applyFunBodyLikeBlock(fun.body))) + collectedFunDefn += FunDefn(N, outerBms, outerDsym, PlainParamList((thisParam.iterator ++ outerParams.iterator.map(_._2)).toList) :: PlainParamList(Nil) :: Nil, newBody)(fun.configOverride, noAsync) + val callArgs = (fun.owner.iterator.map(s => Arg(N, Value.This(s))) ++ fun.params.iterator.flatMap(_.allParams.iterator.map(p => Arg(N, Value.SimpleRef(p.sym))))).toList + val tmp = TempSymbol(N, "tmp") + val wrapperBody = blockBuilder + .assignScoped(tmp, Call(Value.MemberRef(outerBms, outerDsym), callArgs ne_:: Nil)(CallMetadata.mlsFunWithEffect)) + .ret(Call(Value.SimpleRef(State.runtimeSymbol).selSN("toJsAsync"), (tmp.asSimpleRef.asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun)) + FunDefn(fun.owner, fun.sym, fun.dSym, fun.params, wrapperBody)(fun.configOverride, noAsync) + + override def applyMainBlock(main: Block): Block = + collectedFunDefn.foldRight(super.applyMainBlock(main)): (defn, acc) => + Scoped(Set.single(defn.sym), Define(defn, acc)) + + override def applyPath(p: Path)(k: Path => Block): Block = + p match + case s: Select if s.symbol.contains(ctx.builtins.handlers.await) => + if config.effectHandlers.isEmpty && !inAsyncFun then + raise(ErrorReport( + msg"Only await inside of async bodies are allowed if effect handlers are not enabled." -> + p.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + k(State.runtimeSymbol.asSimpleRef.sel(new Tree.Ident("await"), ctx.builtins.handlers.await)) + case _ => + super.applyPath(p)(k) + + def transform(prog: Program): Program = + Rewriter.applyProgram(prog) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index a1ad82c03d..d326dc1188 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -673,8 +673,9 @@ final case class FunDefn( val asPath = sym.asMemberRef(dSym) lazy val tailRec: Bool = annotations.contains(Annot.TailRec) lazy val inline: Bool = annotations.contains(Annot.Inline) - lazy val noInline: Bool = annotations.contains(Annot.NoInline) || generator + lazy val noInline: Bool = annotations.contains(Annot.NoInline) || generator || async lazy val generator: Bool = annotations.contains(Annot.Generator) + lazy val async: Bool = annotations.contains(Annot.Async) lazy val visibility: Visibility = annotations.collectFirst: case Annot.Modifier(Keyword.`private`) => Visibility.Private case Annot.Modifier(Keyword.`public`) => Visibility.Public diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index a384fe53dd..9870cfa2a0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -47,8 +47,8 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): blockPass(Lifter(_).transform)(prog) else prog runPass("HandlerLowering"): prog => - config.effectHandlers.fold(prog): opt => - HandlerLowering(new HandlerPaths, opt).translateProgram(prog) + HandlerLowering(new HandlerPaths, config.effectHandlers).translateProgram(prog) + runPass("AsyncLowering")(AsyncLowering().transform) runPass("Flattening")(blockPass(_.flattened)) runPass("BufferableTransform")(BufferableTransform().transform) runPass("MergeMatchArmTransformer")(MergeMatchArmTransformer.applyProgram) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala index 1a0c759bb3..dde820df53 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala @@ -53,11 +53,14 @@ object HandlerLowering: case Ctor => true case ModCtor(trulyNested) => trulyNested case TopLevel => false + def inAsync = this match + case FunctionLike(ctx) => ctx.inAsync + case _ => false // currentFun: path to the current function for resumption // thisPath: path to `this` binding if the function is a method, `this` will be rebinded on resumption - private case class FunctionCtx(currentFun: Path, thisPath: Option[Path], resumeInfo: ResumeInfo, debugInfo: DebugInfo, inGetter: Bool): + private case class FunctionCtx(currentFun: Path, thisPath: Option[Path], resumeInfo: ResumeInfo, debugInfo: DebugInfo, inGetter: Bool, inAsync: Bool): def doUnwind(loc: Value, state: Path, restoreList: List[LocalVarSymbol])(using paths: HandlerPaths) = Return(Call(paths.unwindPath, ( currentFun :: @@ -117,7 +120,10 @@ class HandlerPaths(using Elaborator.State): val resumeValueIdent = new Tree.Ident("resumeValue") val resumeValue: Path = runtimePath.selN(resumeValueIdent) -class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, Elaborator.State, Elaborator.Ctx, Config): +class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, Raise, Elaborator.State, Elaborator.Ctx, Config): + + val debugEnabled = opt.exists(_.debug) + val stackSafety = opt.flatMap(_.stackSafety) private def freshTmp(dbgNme: Str = "tmp") = new TempSymbol(N, dbgNme) private def freshLabel(nme: Str) = new LabelSymbol(N, nme) @@ -320,7 +326,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, val initId = allocId() // Note: initial part will only be resumed if stack safety is on. - val initPart = BlockPartition(go(blk)(using N, false), opt.stackSafety.isDefined) + val initPart = BlockPartition(go(blk)(using N, false), stackSafety.isDefined) result(initId) = initPart val replaceStaleLabels = new BlockTransformerShallow(SymbolSubst.Id): @@ -511,7 +517,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, ret.sortBy(x => x.headOption.getOrElse(BigInt(-1))).toList private def lifterReport(using Line, FileName)(msgs: Ls[Message -> Opt[Loc]])(using Name) = - if opt.softLifterError then + if opt.fold(false)(_.softLifterError) then WarningReport(msgs, source = Diagnostic.Source.Compilation) else InternalError(msgs, source = Diagnostic.Source.Compilation) @@ -543,7 +549,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, val rtArgLists = intLit(fun.params.length) :: fun.params.flatMap: pl => intLit(pl.params.length) :: pl.params.map(p => p.sym.asSimpleRef) val newCtx = HandlerCtx.FunctionLike(FunctionCtx(funcPath, thisPath, ResumeInfo(rtArgLists, sortedVars, L(fun.sym)), - DebugInfo(debugNme, if opt.debug then debugInfoSym.asSimpleRef else unit), thisPath.isDefined && fun.params.isEmpty)) + DebugInfo(debugNme, if debugEnabled then debugInfoSym.asSimpleRef else unit), thisPath.isDefined && fun.params.isEmpty, fun.async)) val bod2 = translateBlock(fun.body, newCtx, scopedVars) val fun2 = if fun.body is bod2 then fun else FunDefn(fun.owner, fun.sym, fun.dSym, fun.params, bod2)(fun.configOverride, fun.annotations) @@ -559,21 +565,21 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, case _ => super.applyResult(r)(k) override def applyDefn(defn: Defn)(k: Defn => Block): Block = defn match case fun: FunDefn => - if h.currentBlockIsTrulyNested then + if h.currentBlockIsTrulyNested && opt.isDefined then raise(lifterReport(msg"Unexpected nested function: lambdas may not function correctly." -> fun.sym.toLoc :: Nil)) val (debugInfoSym, debugInfo, fun2) = translateFunLike(fun, fun.sym.asMemberRef(fun.dSym), N, fun.sym.nme) - if opt.debug then Scoped(Set.single(debugInfoSym), Assign(debugInfoSym, Tuple(false, debugInfo), k(fun2))) else k(fun2) + if debugEnabled then Scoped(Set.single(debugInfoSym), Assign(debugInfoSym, Tuple(false, debugInfo), k(fun2))) else k(fun2) case defn @ ClsLikeDefn(owner, isym, sym, ctorSym, kind, paramsOpt, auxParams, parentPath, methods, privateFields, publicFields, preCtor, ctor, companion, bufferable) => - if h.currentBlockIsTrulyNested then + if h.currentBlockIsTrulyNested && opt.isDefined then raise(lifterReport(msg"Unexpected nested class: lambdas may not function correctly." -> isym.toLoc :: Nil)) val debugInfos = mutable.ArrayBuffer.empty[(TempSymbol, List[Arg])] - val newMtds = methods.map: f => + val newMtds = methods.mapConserve: f => val (debugInfoSym, debugInfo, fun2) = translateFunLike(f, isym.asThis.sel(new Tree.Ident(f.sym.nme), f.dSym), S(isym.asThis), s"${sym.nme}#${f.sym.nme}") debugInfos += debugInfoSym -> debugInfo fun2 - val companion2 = companion.map: bod => - val newMtds = bod.methods.map: f => + val companion2 = companion.mapConserve: bod => + val newMtds = bod.methods.mapConserve: f => val (debugInfoSym, debugInfo, fun2) = translateFunLike(f, bod.isym.asThis.sel(new Tree.Ident(f.sym.nme), f.dSym), S(bod.isym.asThis), s"${sym.nme}.${f.sym.nme}") debugInfos += debugInfoSym -> debugInfo @@ -583,18 +589,28 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, // TODO: Companion's ctor is more well behaved so it is possible to handle it // However, JSBuilder inserts extra statements between preCtor and ctor and it's not possible to replicate the exact behavior // without many special handling. - val newCtor = if opt.doNotInstrumentTopLevelModCtor && !h.currentBlockIsTrulyNested then bod.ctor else + val newCtor = if opt.fold(true)(_.doNotInstrumentTopLevelModCtor) && !h.currentBlockIsTrulyNested then bod.ctor else translateCtorLike(bod.ctor, bod.isym.asThis, true) tl.log(s"companion name: ${bod.isym.nme}") - ClsLikeBody(bod.isym, newMtds, bod.privateFields, bod.publicFields, newCtor, bod.annotations) - val c2 = ClsLikeDefn(owner, isym, sym, ctorSym, kind, paramsOpt, auxParams, parentPath, newMtds, privateFields, publicFields, - translateCtorLike(preCtor, isym.asThis, false), translateCtorLike(ctor, isym.asThis, false), companion2, bufferable)(defn.configOverride, defn.annotations) - if opt.debug then + if (bod.methods is newMtds) && (bod.ctor is newCtor) then + bod + else + ClsLikeBody(bod.isym, newMtds, bod.privateFields, bod.publicFields, newCtor, bod.annotations) + val newPreCtor = translateCtorLike(preCtor, isym.asThis, false) + val newCtor = translateCtorLike(ctor, isym.asThis, false) + val c2 = + if (methods is newMtds) && (preCtor is newPreCtor) && (ctor is newCtor) && (companion is companion2) then + defn + else + defn.copy(methods = newMtds, preCtor = newPreCtor, ctor = newCtor, companion = companion2)(defn.configOverride, defn.annotations) + if debugEnabled then Scoped(debugInfos.map(_._1).toSet, debugInfos.foldRight(k(c2)): (elem, blk) => Assign(elem._1, Tuple(false, elem._2), blk)) else k(c2) case _ => super.applyDefn(defn)(k) val b = preTransform.applyBlock(blk) + if !opt.isDefined && !h.inAsync then + return b if !h.currentBlockIsTrulyNested then return postTranslateTopLevelCtx(b) if h.inCtor then @@ -604,11 +620,11 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, return postTranslateIllegalEffectCtx(b, "in a getter") given FunctionCtx = ctx val parts = partitionBlock(b) - val needsStackSafety = parts.needsStackSafety && opt.stackSafety.isDefined + val needsStackSafety = parts.needsStackSafety && stackSafety.isDefined val oneState = parts.states.size <= 1 if oneState && !parts.containsError && !needsStackSafety then return b - val vars = if opt.debug then ctx.resumeInfo.currentLocals else computeRestoreList(parts) + val vars = if debugEnabled then ctx.resumeInfo.currentLocals else computeRestoreList(parts) val pcVar = freshTmp("pc") val curDepth = freshTmp("curDepth") @@ -734,7 +750,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, */ private def postTranslateTopLevelCtx(b: Block)(using HandlerCtx): Block = - postTranslateIllegalEffectCtx(b, Call.raw(paths.topLevelEffectPath, (Value.Lit(Tree.BoolLit(opt.debug)).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun), opt.stackSafety.map(_.stackLimit)) + postTranslateIllegalEffectCtx(b, Call.raw(paths.topLevelEffectPath, (Value.Lit(Tree.BoolLit(debugEnabled)).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun), stackSafety.map(_.stackLimit)) private def postTranslateIllegalEffectCtx(b: Block, reason: Str)(using HandlerCtx): Block = postTranslateIllegalEffectCtx(b, Call.raw(paths.illegalEffectPath, (Value.Lit(Tree.StrLit(reason)).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun), N) @@ -781,7 +797,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise, val ctx = HandlerCtx.TopLevel val transformed = blockBuilder .staticif( - !opt.doNotInstrumentTopLevelModCtor, + opt.fold(false)(!_.doNotInstrumentTopLevelModCtor), _.assign(NoSymbol, Call(paths.resetEffects, Nil ne_:: Nil)(CallMetadata.defaultMlsFun)) ) .rest(translateBlock(prog.main, ctx, Set.empty)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 5ae20fa9d9..90f9200e7f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -707,6 +707,28 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): @tailrec def extractAnnots(t: st, acc: List[Annot]): (List[Annot], st) = t match + case st.Annotated(Annot.Async, trm) => + val ident = new Tree.Ident("asyncBody").withLocOf(trm) + val bms = BlockMemberSymbol(ident.name, Nil, false) + val dsym = TermSymbol(syntax.Fun, N, ident) + val td: TermDefinition = TermDefinition( + syntax.Fun, + bms, + dsym, + sem.ParamList(sem.ParamListFlags.empty, Nil, N) :: Nil, + N, + N, + S(trm), + TermDefFlags.empty, + Modulefulness.none, + Annot.RaiseEffects :: Nil, + N, + ) + val rewritten = st.App( + st.SynthSel(State.runtimeSymbol.ref(), Tree.Ident("toJsAsync"))(N, FlowSymbol.sel("toJsAsync"), N, N), + st.Tup(PlainFld(st.Blk(td :: Nil, bms.ref(ident).resolved(dsym))) :: Nil)(Tree.DummyTup) + )(Tree.DummyApp, N, FlowSymbol.app()) + extractAnnots(rewritten, acc) case st.Annotated(annot, trm) => extractAnnots(trm.instantiated, annot :: acc) case _ => (acc, t) @@ -913,7 +935,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): conclude(Select(p, definitionIdent(nme, sym))(S(sym))(false).withLocOf(sel)) case _ => subTerm(baseF)(conclude) case h @ Handle(lhs, rhs, as, cls, defs, bod) => - if !lowerHandlers then + if config.effectHandlers.isEmpty then return fail: ErrorReport( msg"Effect handlers are not enabled" -> @@ -1416,12 +1438,14 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case N => WarningReport(msg"This annotation has no effect." -> annot.toLoc :: Nil) annotations.foreach: case Annot.Untyped => () - case a @ (Annot.TailRec | Annot.Inline | Annot.NoInline | Annot.Generator) => + case a @ (Annot.TailRec | Annot.Inline | Annot.NoInline | Annot.Generator | Annot.Async | Annot.RaiseEffects) => val annot = a match case Annot.TailRec => "@tailrec" case Annot.Inline => "@inline" case Annot.NoInline => "@noInline" case Annot.Generator => "@generator" + case Annot.Async => "@async" + case Annot.RaiseEffects => "@raiseEffects" target match case TermDefinition(body = S(bod), k = syntax.Fun) => () case TermDefinition(k = syntax.Fun) => warn(a, S(msg"Only functions with a body may be marked as $annot.")) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 00e8f8bd28..d10aa79d29 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -46,14 +46,14 @@ object Elaborator: // TODO: rename to ScopeKind? enum OuterCtx: - case Function(returnHandlerSymbol: TempSymbol, isGenerator: Bool) + case Function(returnHandlerSymbol: TempSymbol)(val isGenerator: Bool, val isAsync: Bool) case InnerScope(innerSymbol: InnerSymbol) case LocalScope(nameHint: Str) case LambdaOrHandlerBlock case NonReturnContext def showDbg: Str = this match - case Function(sym, _) => s"fun:${sym.nme}" + case Function(sym) => s"fun:${sym.nme}" case InnerScope(inner) => inner.toString case LocalScope(hint) => hint case LambdaOrHandlerBlock => "LambdaOrHandlerBlock" @@ -178,20 +178,26 @@ object Elaborator: go(S(this), false, false) def getOuter: Opt[InnerSymbol] = outer.inner.orElse(parent.flatMap(_.getOuter)) def getNonLocalRetHandler: Opt[TempSymbol] = outer match - case OuterCtx.Function(sym, _) => S(sym) + case OuterCtx.Function(sym) => S(sym) case _ => parent.flatMap(_.getNonLocalRetHandler) def getRetHandler: ReturnHandler = outer match - case OuterCtx.Function(sym, _) => ReturnHandler.Direct + case OuterCtx.Function(sym) => ReturnHandler.Direct case _: (OuterCtx.LambdaOrHandlerBlock.type | OuterCtx.InnerScope) => getNonLocalRetHandler.fold(ReturnHandler.NotInFunction)(ReturnHandler.Required(_)) case OuterCtx.NonReturnContext => ReturnHandler.Forbidden case _: OuterCtx.LocalScope => parent.fold(ReturnHandler.NotInFunction)(_.getRetHandler) def inGenerator: Bool = outer match - case OuterCtx.Function(_, isGenerator) => isGenerator + case f: OuterCtx.Function => f.isGenerator case _: OuterCtx.LocalScope => parent.fold(false)(_.inGenerator) case _: (OuterCtx.LambdaOrHandlerBlock.type | OuterCtx.InnerScope | OuterCtx.NonReturnContext.type) => false + def inAsync: Bool = outer match + case f: OuterCtx.Function => f.isAsync + case _: OuterCtx.LocalScope => + parent.fold(false)(_.inAsync) + case _: (OuterCtx.LambdaOrHandlerBlock.type | OuterCtx.InnerScope | OuterCtx.NonReturnContext.type) => false + def potentiallyInstrumented(using Config): Bool = config.effectHandlers.isDefined || inAsync // * Invariant: We expect that the top-level context only contain hard-coded symbols like `globalThis` // * and that built-in symbols like Int and Str be imported into another nested context on top of it. @@ -286,10 +292,13 @@ object Elaborator: val inline = assumeObject("inline") val noInline = assumeObject("noInline") val generator = assumeObject("generator") + val async = assumeObject("async") val compile = assumeObject("compile") val buffered = assumeObject("buffered") val bufferable = assumeObject("bufferable") val mayNotRaiseEffects = assumeObject("mayNotRaiseEffects") + object handlers extends VirtualModule(assumeBuiltinMod("handlers")): + val await = assumeObject("await").asTrm.get object scope extends VirtualModule(assumeBuiltinMod("scope")): val locally = assumeObject("locally") object runtime extends VirtualModule(assumeBuiltinMod("runtime")): @@ -613,6 +622,8 @@ extends Importer: return S(Annot.NoInline) case ctx.builtins.annotations.generator => return S(Annot.Generator) + case ctx.builtins.annotations.async => + return S(Annot.Async) case ctx.builtins.annotations.mayNotRaiseEffects => return S(Annot.MayNotRaiseEffects) case _ => () @@ -1053,7 +1064,7 @@ extends Importer: error case LetLike(Keywrd(`set`), lhs, S(rhs), S(bod)) => // * Backtracking assignment - if config.effectHandlers.isDefined then + if ctx.potentiallyInstrumented then raise(ErrorReport( msg"Backtracking assignment is not supported with effect handlers enabled" -> tree.toLoc :: Nil)) @@ -1239,7 +1250,7 @@ extends Importer: case LabelLookup.Found(binding) => Term.Break(binding.labelSymbol, binding.resultSymbol, value) case LabelLookup.AcrossBoundary(binding) => - if config.effectHandlers.isEmpty then + if !ctx.potentiallyInstrumented then mkNonLabelSelectionApp(tree, sel, args) else markEffectMethodUsed(binding.nonLocalBreakMethodMarker, nme) @@ -1261,7 +1272,7 @@ extends Importer: Term.Continue(binding.labelSymbol) case LabelLookup.AcrossBoundary(binding) => checkNoArgs - if config.effectHandlers.isEmpty then + if !ctx.potentiallyInstrumented then raise: ErrorReport(msg"Non-local 'continue' is only supported with effect handlers enabled." -> labelId.toLoc :: Nil) @@ -1293,7 +1304,7 @@ extends Importer: case LabelLookup.Found(binding) => Term.Break(binding.labelSymbol, binding.resultSymbol, N) case LabelLookup.AcrossBoundary(binding) => - if config.effectHandlers.isEmpty then + if !ctx.potentiallyInstrumented then raise: ErrorReport(msg"Non-local 'break' is only supported with effect handlers enabled." -> labelId.toLoc :: Nil) @@ -1308,7 +1319,7 @@ extends Importer: case LabelLookup.Found(binding) => Term.Continue(binding.labelSymbol) case LabelLookup.AcrossBoundary(binding) => - if config.effectHandlers.isEmpty then + if !ctx.potentiallyInstrumented then raise: ErrorReport(msg"Non-local 'continue' is only supported with effect handlers enabled." -> labelId.toLoc :: Nil) @@ -1418,7 +1429,7 @@ extends Importer: ctx.getRetHandler match case ReturnHandler.Required(sym) => log(s"Non-local return: $sym") - if config.effectHandlers.isEmpty then + if !ctx.potentiallyInstrumented then raise: ErrorReport(msg"Non-local return statements are only supported with effect handlers enabled." -> tree.toLoc :: Nil) error @@ -1916,9 +1927,10 @@ extends Importer: case S(rhs) => S: val nonLocalRetHandler = TempSymbol(N, s"nonLocalRetHandler$$${id.name}") val hasGeneratorAnnotation = annotations.contains(Annot.Generator) + val hasAsyncAnnotation = annotations.contains(Annot.Async) if pss.isEmpty && hasGeneratorAnnotation then raise(ErrorReport(msg"Generators are not supported on functions without a parameter list" -> td.toLoc :: Nil)) - newCtx.nest(OuterCtx.Function(nonLocalRetHandler, pss.nonEmpty && hasGeneratorAnnotation)).givenIn: newCtx ?=> + newCtx.nest(OuterCtx.Function(nonLocalRetHandler)(pss.nonEmpty && hasGeneratorAnnotation, hasAsyncAnnotation)).givenIn: newCtx ?=> val b = term(rhs)(using newCtx) if nonLocalRetHandler.directRefs.isEmpty then b else mkEffectHandleAbortive( diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index bb3c3b1450..5ebd0b8d26 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -29,6 +29,8 @@ enum Annot extends AutoLocated: case Inline case NoInline case Generator + case Async + case RaiseEffects // Whether the function is guaranteed to not raise effects. case MayNotRaiseEffects case Config(modify: hkmc2.Config => hkmc2.Config) @@ -51,19 +53,21 @@ enum Annot extends AutoLocated: def subTerms: Vector[Term] = this match case Trm(trm) => Vector.single(trm) case _: Modifier | Untyped | TailRec | TailCall | Inline | NoInline - | Generator | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty + | Generator | Async | RaiseEffects | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty def children: Vector[Located] = this match case Trm(trm) => Vector.single(trm) // case Modifier(kw) => Vector.single(kw) // TODO: make `kw` a `Keywrd` case _: Modifier | Untyped | TailRec | TailCall | Inline | NoInline - | Generator | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty + | Generator | Async | RaiseEffects | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty def show(using Scope, ShowCfg, Raise): Document = this match case Untyped => doc"@untyped" case Inline => doc"@inline" case NoInline => doc"@noInline" case Generator => doc"@generator" + case Async => doc"@async" + case RaiseEffects => doc"@raiseEffects" case TailRec => doc"@tailrec" case TailCall => doc"@tailcall" case Affine(n) => doc"@affine($n)" @@ -81,6 +85,8 @@ enum Annot extends AutoLocated: case Inline => Inline case NoInline => NoInline case Generator => Generator + case Async => Async + case RaiseEffects => RaiseEffects case MayNotRaiseEffects => MayNotRaiseEffects case c: Config => c case a: Affine => a diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mls b/hkmc2/shared/src/test/mlscript-compile/Predef.mls index 3c11986f5b..b02ae78268 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mls @@ -143,7 +143,6 @@ fun enterHandleBlock(handler, body) = fun raiseUnhandledEffect() = Runtime.mkEffect(Runtime.FatalEffect, null) - module meta with fun codegen(t, file) = Term.codegen(t, file) fun print(t) = Term.print(t) diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index d87c09cd60..3b63a488c7 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -5,7 +5,34 @@ import RuntimeJS from "./RuntimeJS.mjs"; import Rendering from "./Rendering.mjs"; import LazyArray from "./LazyArray.mjs"; import Iter from "./Iter.mjs"; -let Runtime1, lambda, lambda1, lambda2, lambda3, lambda4, lambda$, lambda$1, Capture$scope301, lambda$2, Capture$scope321, lambda$3; +let continuation, Runtime1, lambda, lambda1, lambda2, lambda3, lambda4, lambda5, lambda$, lambda$1, Capture$scope301, lambda$2, Capture$scope321, lambda$3, lambda$4, continuation$; +continuation$ = function continuation$(Runtime2, resume) { + return (value) => { + return continuation(Runtime2, resume, value) + } +}; +continuation = function continuation(Runtime2, resume, value) { + let r, scrut; + r = runtime.safeCall(resume(value)); + scrut = Runtime2.curEffect !== null; + if (scrut === true) { + Runtime2.illegalEffect("in exported async function"); + return r + } + return r; +}; +lambda$4 = (undefined, function (Runtime2, promise) { + return (resume) => { + let continuation$here; + continuation$here = continuation$(Runtime2, resume); + return runtime.safeCall(promise.then(continuation$here)) + } +}); +lambda3 = (undefined, function (Runtime2, promise, resume) { + let continuation$here; + continuation$here = continuation$(Runtime2, resume); + return runtime.safeCall(promise.then(continuation$here)) +}); (class Capture$scope32 { static { Capture$scope321 = this @@ -70,7 +97,7 @@ lambda$1 = (undefined, function (Runtime2) { return runtime.Unit } }); -lambda4 = (undefined, function (Runtime2, k) { +lambda5 = (undefined, function (Runtime2, k) { Runtime2.stackResume = k; return runtime.Unit }); @@ -79,7 +106,7 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return Runtime2.resume(EffectHandle1.reified.contTrace)(value) } }); -lambda3 = (undefined, function (Runtime2, EffectHandle1, value) { +lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { return Runtime2.resume(EffectHandle1.reified.contTrace)(value) }); (class Runtime { @@ -568,6 +595,20 @@ lambda3 = (undefined, function (Runtime2, EffectHandle1, value) { [prettyPrint]() { return this.toString(); } static [definitionMetadata] = ["class", "CustomStackError", ["stack"]]; }); + (class AsyncEffectMarker { + static { + new this + } + constructor() { + Runtime.AsyncEffectMarker = this; + Object.defineProperty(this, "class", { + value: AsyncEffectMarker + }); + globalThis.Object.freeze(this); + } + toString() { return runtime.render(this); } + static [definitionMetadata] = ["object", "AsyncEffectMarker"]; + }); Runtime.stackLimit = 0; Runtime.stackDepth = 0; Runtime.stackHandler = null; @@ -1133,6 +1174,21 @@ lambda3 = (undefined, function (Runtime2, EffectHandle1, value) { return value; } } + static await(promise) { + let lambda$here; + lambda$here = lambda$4(Runtime, promise); + return Runtime.mkEffect(Runtime.AsyncEffectMarker, lambda$here) + } + static toJsAsync(thunk) { + let r, scrut; + r = Runtime.enterHandleBlock(Runtime.AsyncEffectMarker, thunk); + scrut = Runtime.curEffect !== null; + if (scrut === true) { + Runtime.illegalEffect("in exported async function"); + return runtime.safeCall(globalThis.Promise.resolve(r)) + } + return runtime.safeCall(globalThis.Promise.resolve(r)); + } static checkDepth() { let tmp, tmp1; tmp = Runtime.stackDepth >= Runtime.stackLimit; @@ -1204,16 +1260,20 @@ lambda3 = (undefined, function (Runtime2, EffectHandle1, value) { toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Runtime"]; }); +export { continuation as _$_modulePrivate_$_continuation }; export { Runtime1 as _$_modulePrivate_$_Runtime }; export { lambda as _$_modulePrivate_$_lambda }; export { lambda1 as _$_modulePrivate_$_lambda1 }; export { lambda2 as _$_modulePrivate_$_lambda2 }; export { lambda3 as _$_modulePrivate_$_lambda3 }; export { lambda4 as _$_modulePrivate_$_lambda4 }; +export { lambda5 as _$_modulePrivate_$_lambda5 }; export { lambda$ as _$_modulePrivate_$_lambda$ }; export { lambda$1 as _$_modulePrivate_$_lambda$1 }; export { Capture$scope301 as _$_modulePrivate_$_Capture$scope30 }; export { lambda$2 as _$_modulePrivate_$_lambda$2 }; export { Capture$scope321 as _$_modulePrivate_$_Capture$scope32 }; export { lambda$3 as _$_modulePrivate_$_lambda$3 }; +export { lambda$4 as _$_modulePrivate_$_lambda$4 }; +export { continuation$ as _$_modulePrivate_$_continuation$ }; let Runtime = Runtime1; export default Runtime; diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls index 290d3d99d6..9bc17339d6 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls @@ -473,6 +473,25 @@ fun resumeContTrace(contTrace, value) = else return value +// js async +object AsyncEffectMarker + +fun await(promise) = + mkEffect(AsyncEffectMarker, resume => + fun continuation(value) = + let r = resume(value) + if curEffect !== null do + illegalEffect("in exported async function") + r + promise.then(continuation) + ) + +fun toJsAsync(thunk) = + let r = enterHandleBlock(AsyncEffectMarker, thunk) + if curEffect !== null do + illegalEffect("in exported async function") + Promise.resolve(r) + // stack safety mut val stackLimit = 0 // How deep the stack can go before heapifying the stack mut val stackDepth = 0 diff --git a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls index 8b96e76ac4..544c614cb2 100644 --- a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls +++ b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls @@ -276,6 +276,10 @@ declare module annotations with object bufferable object mayNotRaiseEffects object generator + object async + +declare module handlers with + fun await declare module scope with fun locally diff --git a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls index 65dc532bd6..4c879518fa 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls @@ -97,7 +97,7 @@ fun c(x) = if x is AA(_) then 0 let p = AA(10) in c(p) + c(p) //│ deforest > >>> non-affine syms >>> -//│ deforest > p@1 +//│ deforest > p@2 //│ deforest > <<< non-affine syms <<< //│ deforest > >>> fusing >>> //│ deforest > <<< fusing <<< diff --git a/hkmc2/shared/src/test/mlscript/handlers/Async.mls b/hkmc2/shared/src/test/mlscript/handlers/Async.mls new file mode 100644 index 0000000000..54088940f3 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/handlers/Async.mls @@ -0,0 +1,111 @@ +:js +:lift + +import "fs" + +open annotations +open handlers + +@async +fun f(print_result) = + print("Hello, world!") + let r = await(fs.promises.readFile("./README.md")) + let readme = r.toString() + let first = readme.slice(0, readme.indexOf("\n")) + if print_result do + print(first) + first + +// Should get a promise here, the promise is used for JavaScript interop +f(false) +//│ > Hello, world! +//│ = Promise {} + + +// :await will await the promise returned at the top level for the repl +:await +f(true) +//│ > Hello, world! +//│ > # MLscript +//│ = "# MLscript" + +:re +:await +@async +fun f() = + raiseUnhandledEffect() +f() +//│ ═══[RUNTIME ERROR] Error: Effect FatalEffect is raised in exported async function + +:ge +fun f() = + await(Promise.resolve(1)) +//│ ╔══[COMPILATION ERROR] Only await inside of async bodies are allowed if effect handlers are not enabled. +//│ ║ l.42: await(Promise.resolve(1)) +//│ ╙── ^^^^^ + +:re +:await +@async +fun f() = + print(0) + let x = await(Promise.resolve(1)) + raiseUnhandledEffect() + print(1) +f() +//│ > 0 +//│ ═══[RUNTIME ERROR] Error: Effect FatalEffect is raised in exported async function +//│ = Promise {} + +:re +:await +@async +fun f() = + print(0) + let x = await(Promise.reject("Promise error")) + raiseUnhandledEffect() + print(1) +f() +//│ > 0 +//│ ═══[RUNTIME ERROR] Promise error +//│ = Promise {} + +:global +:effectHandlers + +fun f() = + print(0) + let r = await(fs.promises.readFile("./README.md")) + let readme = r.toString() + let first = readme.slice(0, readme.indexOf("\n")) + print(first) + let r2 = await(fs.promises.readdir(".")) + print(r2.indexOf("README.md") >= 0) + +@async +fun g() = + f() + f() + +:await +g() +//│ > 0 +//│ > # MLscript +//│ > true +//│ > 0 +//│ > # MLscript +//│ > true + +@async +fun g() = + await(Promise.all([@async f(), @async f()])) + +:await +g() +//│ > 0 +//│ > 0 +//│ > # MLscript +//│ > # MLscript +//│ > true +//│ > true +//│ = [(), ()] diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls index e13b9f4064..2bf32486be 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls @@ -91,6 +91,10 @@ declare module annotations with object bufferable object mayNotRaiseEffects object generator + object async + +declare module handlers with + fun await declare module scope with fun locally diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 12f7be7996..38d7e286bd 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -24,6 +24,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val showSanitizedJS = NullaryCommand("ssjs") val showJS = NullaryCommand("sjs") val showRepl = NullaryCommand("showRepl") + val await = NullaryCommand("await") val traceJS = NullaryCommand("traceJS") val expect = Command("expect"): ln => ln.trim @@ -268,7 +269,8 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: // * Sometimes the JS block won't execute due to a syntax or runtime error so we always set this first host.execute(s"$resNme = undefined") - mkQuery(preStr, jsStr): stdout => + val awaitResult = (if await.isSet then s"; $resNme = await $resNme" else "") + mkQuery(preStr, jsStr + awaitResult): stdout => stdout.splitSane('\n').init // should always ends with "undefined" (TODO: check) .foreach: line => output(s"> ${line}")