diff --git a/doc/reference.md b/doc/reference.md index 0260f5b779..63c8375344 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -734,6 +734,10 @@ new! c // dynamic instantiation (class value at runtime) new Foo(...xs) // spread args ``` +`new mut` is an unsafe escape hatch. Optimizations may still assume that `val` +fields are immutable; mutating them can therefore change optimized program behavior. +Use `mut val` for fields whose updates must be observed by ordinary code. + --- ## 8. Objects, Modules, and Companions diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 1c8423d582..bd308c625c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -33,6 +33,7 @@ case class Config( commentGeneratedCode: Bool, noFreeze: Bool, noModuleCheck: Bool, + aggressive: Bool, debug: Debug, optimizer: Optimizer, // Check internal invariants whose checks are too costly to always run, such as MemberStorage.privateOwner's. @@ -88,6 +89,7 @@ object Config: commentGeneratedCode = false, noFreeze = false, noModuleCheck = false, + aggressive = false, debug = Debug.default, optimizer = Optimizer.FastOpt, checkInvariants = false, @@ -264,6 +266,7 @@ object Config: debugEta: Bool = false, debugDpe: Bool = false, debugDce: Bool = false, + logEffects: Bool = false, ): def effectiveDebugEta: Bool = debug || debugEta def effectiveDebugDpe: Bool = debug || debugDpe @@ -719,6 +722,7 @@ object ConfigParser: var debugEta = base.debugEta var debugDpe = base.debugDpe var debugDce = base.debugDce + var logEffects = base.logEffects args.foreach: case NamedArg("debug", value) => setFrom(value)(parseBool)(v => debug = v) @@ -738,6 +742,8 @@ object ConfigParser: setFrom(value)(parseBool)(v => debugDpe = v) case NamedArg("debugDce", value) => setFrom(value)(parseBool)(v => debugDce = v) + case NamedArg("logEffects", value) => + setFrom(value)(parseBool)(v => logEffects = v) case other => unsupported(passName, other) S(Config.FlowAnalysisConfig( @@ -750,6 +756,7 @@ object ConfigParser: debugEta, debugDpe, debugDce, + logEffects, )) case _ => expect(s"${passName}(...)")(tree) @@ -784,6 +791,8 @@ object ConfigParser: parsedField(value)(parseBool)(v => _.copy(noFreeze = v)) case "noModuleCheck" => parsedField(value)(parseBool)(v => _.copy(noModuleCheck = v)) + case "aggressive" => + parsedField(value)(parseBool)(v => _.copy(aggressive = v)) case "rewriteWhileLoops" => parsedField(value)(parseBool)(v => _.copy(rewriteWhileLoops = v)) case "stageCode" => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index 53152fadc5..410fe97453 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -130,7 +130,7 @@ sealed abstract class Block extends Product: case Define(defn, rest) => s"""|Define(${defn.showDbg}) |${rest.showDbg}""".stripMargin - case Throw(exc) => s"Throw(${exc.showDbg})" + case Throw(exc, _) => s"Throw(${exc.showDbg})" case Scoped(syms, body) => s"""|Scoped(${syms.toList.map(_.showDbg).sorted.mkString(", ")}) { |${body.showDbg} @@ -177,7 +177,7 @@ sealed abstract class Block extends Product: lazy val size: Int = 1 + this.match case Return(r: Result) => r.size - case Throw(r: Result) => r.size + case Throw(r: Result, _) => r.size case _: End | _: Break | _: Continue | _: Unreachable => 0 case Begin(sub, rst) => sub.size + rst.size case Assign(_, r, rst) => r.size + rst.size @@ -206,7 +206,7 @@ sealed abstract class Block extends Product: ++ arms.iterator.flatMap: (pat, arm) => arm.freeVars ++ pat.freeVars case Return(res) => res.freeVars - case Throw(exc) => exc.freeVars + case Throw(exc, _) => exc.freeVars case Label(label, _, body, rest) => (body.freeVars - label) ++ rest.freeVars case Break(label) => Set.single(label) case Continue(label) => Set.single(label) @@ -238,7 +238,7 @@ sealed abstract class Block extends Product: // TODO rm Lam from results and thus the need for these cases case Return(r) => r.subBlocks - case Throw(r) => r.subBlocks + case Throw(r, _) => r.subBlocks case _: Return | _: Throw | _: Break | _: Continue | _: End | _: Unreachable => Nil @@ -404,7 +404,7 @@ case class Match( case class Return(res: Result) extends BlockTail -case class Throw(exc: Result) extends BlockTail +case class Throw(exc: Result, annotations: Ls[Annot]) extends BlockTail object Throw: /** Throws a runtime `Error` carrying `msg`. @@ -416,7 +416,7 @@ object Throw: mut = false, State.globalThisSymbol.asThis.selN(Tree.Ident("Error")), (Value.Lit(Tree.StrLit(msg))(N).asArg :: Nil) :: Nil, - )(InstantiateMetadata.empty, N)) + )(InstantiateMetadata.empty, N), annotations = Nil) case class Label(label: LabelSymbol, loop: Bool, body: Block, rest: Block) extends Block with NonBlockTail with ProductWithTail @@ -1082,9 +1082,9 @@ sealed abstract class Result extends Located, HasErasedType: lazy val isPure: Bool = this match case _: Value => true case sel @ Select(q, n) => - q.isPure && sel.symbol.exists(_.isPure) - case DynSelect(q, fld, DynSelectKind.Member(sym)) => q.isPure && fld.isPure && sym.isPure - case sel @ SuperSelect(q, base, key) => q.isPure && base.isPure && key.isPure && sel.symbol.isPure + q.isPure && sel.symbol.exists(_.isAccessPure) + case DynSelect(q, fld, DynSelectKind.Member(sym)) => q.isPure && fld.isPure && sym.isAccessPure + case sel @ SuperSelect(q, base, key) => q.isPure && base.isPure && key.isPure && sel.symbol.isAccessPure case c @ Call(fun, ass) if c.isKnownUnsaturatedCall => fun.isPure && ass.forall(_.forall(a => a.spread.isEmpty && a.value.isPure)) case Call(Value.SimpleRef(bs: BuiltinSymbol), ass) if bs.isPure => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala index 23e151a6df..e3b1ea5d5b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala @@ -43,9 +43,9 @@ class BlockTransformer(subst: SymbolSubst): case Return(res) => applyResult(res): res2 => if res2 is res then b else Return(res2) - case Throw(exc) => + case Throw(exc, annotations) => applyResult(exc): exc2 => - if exc2 is exc then b else Throw(exc2) + if exc2 is exc then b else Throw(exc2, annotations) case Match(scrut, arms, dflt, rst) => def applySub(b: Block) = if rst.isEmpty then applySubBlock(b) else applySubBlockNonTail(b) applyPath(scrut): scrut2 => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala index f1c54d431a..2ba58a0149 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala @@ -37,7 +37,7 @@ class BlockTraverser: case Break(lbl) => applySymbol(lbl) case Continue(lbl) => applySymbol(lbl) case Return(res) => applyResult(res) - case Throw(exc) => applyResult(exc) + case Throw(exc, _) => applyResult(exc) case Match(scrut, arms, dflt, rst) => val scrut2 = applyPath(scrut) arms.foreach: arm => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index f6ea3bc447..acf7ec55c0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -97,7 +97,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo EtaTargets(a.paramCount, a.requiredParamCount, a.hasRestParam, a.prodFuns ++ b.prodFuns) go(S(mergedRes)) else Nil - case UnknownProd => Nil + case UnknownProd | UnsafeEta | NoProd => Nil case _: Ctor => Nil end go @@ -107,7 +107,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo case _ => false then go(N) else Nil - case UnknownProd => Nil + case UnknownProd | UnsafeEta | NoProd => Nil case _: Ctor => Nil end funResShape @@ -116,7 +116,10 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo case N => val targets = EtaTargets(pf.params.size, pf.requiredParamCount, pf.restParam.isDefined, Set.single(pf)) if !processing.contains(pf) then - val res = targets :: funResShape(pf.res) + // It'd be unsound if eta-expansion postponed the evaluation of a function + // that may have or observe (side) effects. + val safe = EffectAnalysis.summarize(pf.effect) == EffectSummary.Pure + val res = targets :: (if safe then funResShape(pf.res) else Nil) cache(pf) = res res else diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala index b6bcbd0606..16000d1d0c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala @@ -118,7 +118,11 @@ class FlowAnalysisBasedRewrite( val body2 = withEliminatedParams(removed): withEtaArgss(etaParams.map(_.args)): applyFunBodyLikeBlock(body) - params2 -> body2 + // Calls added for the new parameter lists may be effectful even when the original body was pure. + val annotations2 = + if etaParams.nonEmpty then fun.annotations.filterNot(_.isInstanceOf[Annot.Pure]) + else fun.annotations + (params2, body2, annotations2) // traversal @@ -147,7 +151,9 @@ class FlowAnalysisBasedRewrite( Return(etaCall(p).withLocOf(res2)) case c @ Call(fun, argss) => Return( - Call(fun, (argss ++ activeEtaArgss).ne_!)(c.metadata, c.toLoc)) + Call(fun, (argss ++ activeEtaArgss).ne_!)(c.metadata.copy( + mayRaiseEffects = true, + annotations = c.metadata.annotations.filterNot(_.isInstanceOf[Annot.Pure])), c.toLoc)) case _ => val tmp = TempSymbol(N, erasedType = N, "eta$res") Scoped( @@ -231,9 +237,10 @@ class FlowAnalysisBasedRewrite( val own2 = fun.owner.mapConserve(_.subst) val sym2 = fun.sym.subst val dSym2 = fun.dSym.subst - val (params2, body2) = rewriteFunDefn(fun) + val (params2, body2, annotations2) = rewriteFunDefn(fun) if (own2 is fun.owner) && (sym2 is fun.sym) && (dSym2 is fun.dSym) && (params2 is fun.params) && (body2 is fun.body) - then fun else FunDefn(own2, sym2, dSym2, params2, body2)(fun.configOverride, fun.annotations) + && (annotations2 is fun.annotations) + then fun else FunDefn(own2, sym2, dSym2, params2, body2)(fun.configOverride, annotations2) override def applyLam(lam: Lambda): Lambda = val lamId: ConcreteFunId = ConcreteId(lam.uid, instId) @@ -261,9 +268,9 @@ class FlowAnalysisBasedRewrite( new Rewriter(Nil, N): override def applyFunDefn(fun: FunDefn): FunDefn = rewrittenInPlace.get(fun.dSym) match - case S((params, body)) => - if (params is fun.params) && (body is fun.body) then fun - else FunDefn(fun.owner, fun.sym, fun.dSym, params, body)(fun.configOverride, fun.annotations) + case S((params, body, annotations)) => + if (params is fun.params) && (body is fun.body) && (annotations is fun.annotations) then fun + else FunDefn(fun.owner, fun.sym, fun.dSym, params, body)(fun.configOverride, annotations) case N => super.applyFunDefn(fun) def otherNewFunDefns: Iterable[FunDefn] = Nil @@ -305,10 +312,11 @@ object FlowAnalysisBasedRewrite: mono = optCfg.mono, nonAffineTracking = optCfg.config.effectiveTrackNonAffine, accumulatorTracking = optCfg.config.effectiveTrackAccumulator, + logEffects = optCfg.config.logEffects, ) val etaExpansionSolver = - if eta then new EtaExpansionSolver( + if eta && (cfg.liftDefns.isDefined || cfg.deforest.isDefined) then new EtaExpansionSolver( flowAnalysisRes, mkTl("eta-expansion > ", optCfg.effectiveDebugEta)) else NoEtaExpansion val deadParamElimSolver = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala index 0d952592e9..ad6d5f8932 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala @@ -331,7 +331,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R msg"`try`-`finally` blocks are not currently supported with effect handlers enabled." -> N :: Nil, source = Diagnostic.Source.Compilation)) - case Throw(_) => blk + case Throw(_, _) => blk case Scoped(_, body) => go(body) // PreHandlerLowering val initId = allocId() diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala index abdfae2b87..a2039e878d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala @@ -1105,6 +1105,10 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): val call = Call(fun.sym.asMemberRef(fun.dSym), args ne_:: Nil)(CallMetadata.mlsFunWithEffect, N) val bod = Return(call) + // The new capture list shifts the parameter list indices used by affine annotations. + val annotations = fun.annotations.map: + case a @ Annot.Affine(n) => Annot.Affine(n + 1)(a.toLoc) + case a => a FunDefn( N, @@ -1112,7 +1116,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): auxDsym, newPlists, bod - )(N, if fun.noInline then fun.annotations else Annot.Inline()(N) :: fun.annotations) + )(N, if fun.noInline then annotations else Annot.Inline()(N) :: annotations) private val aux = Lazy[Defn](mkAuxDefn) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index bca4744bca..8885f06c2c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -33,7 +33,7 @@ object ImplctRet extends TailOp(transfersControl = false): case Value.Lit(Tree.UnitLit(false)) => End() case _ => Return(r) object Thrw extends TailOp(transfersControl = true): - def apply(r: Result): Block = Throw(r) + def apply(r: Result): Block = Throw(r, annotations = Nil) class LoweringCtx( @@ -76,7 +76,7 @@ import LoweringCtx.loweringCtx object Lowering: def compError: Block = - Throw(Value.Lit(Tree.StrLit("This code cannot be run as its compilation yielded an error."))(N)) + Throw(Value.Lit(Tree.StrLit("This code cannot be run as its compilation yielded an error."))(N), annotations = Nil) def fail(err: ErrorReport)(using Raise): Block = raise(err) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala index a0b75f2afb..f6a58cf2a8 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala @@ -19,7 +19,7 @@ abstract class FlowAnalysisSolverResult: end FlowAnalysisSolverResult -type RewrittenFunDefn = (params: Ls[LoweredParamList], body: Block) +type RewrittenFunDefn = (params: Ls[LoweredParamList], body: Block, annotations: Ls[Annot]) abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolver): @@ -90,8 +90,8 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv (referringFun, (bms, tSym)) <- funSymMap.toList.sortBy(_._1.uid) yield val original = pre.res.funSymToFunDefn(referringFun) - val (params, body) = mkCopyRewriter(instId, original.params, original.body, Map.empty).rewriteFunDefn(original) - FunDefn(N, bms, tSym, params, body)(original.configOverride, Annot.Private :: original.annotations) + val (params, body, annotations) = mkCopyRewriter(instId, original.params, original.body, Map.empty).rewriteFunDefn(original) + FunDefn(N, bms, tSym, params, body)(original.configOverride, Annot.Private :: annotations) val newFuns = newPolyFuns ++ otherNewFunDefns diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index 290d61d698..17dc0f9a4f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -73,7 +73,7 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): val docDefault = dflt.fold(doc"")(e => doc" # else #{ # ${print(e)} #} ") doc"match ${print(scrut)} #{ # ${docCases}$docDefault #} # ${print(rest)}" case Return(res) => doc"return ${print(res)}" - case Throw(exc) => doc"throw ${print(exc)}" + case Throw(exc, annotations) => doc"${printAnnotations(annotations, doc" ")}throw ${print(exc)}" case Label(label, loop, body, rest) => val l2 = scope.allocateOrGetName(label) // * ^ if we print the same block with a top-level label more than once, using `scope.allocateName` will crash... diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala index 86077c27db..0a0ccd8051 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala @@ -152,6 +152,7 @@ object Deforest: mono = dCfg.mono, nonAffineTracking = dCfg.effectiveTrackNonAffine, accumulatorTracking = dCfg.effectiveTrackAccumulator, + logEffects = dCfg.config.logEffects, ) val solver = new DeforestFusionSolver(flowAnalysisRes) if !solver.hasWorkToDo then p diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala index d1e9166754..fdcc6f631d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala @@ -304,8 +304,8 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo // from the polymorphic analysis protected class Rewriter(instId: InstantiationId, refreshSymbolMappingForCopy: Opt[collection.Map[LocalVarSymbol, LocalVarSymbol]]) extends InstantiationRewriter(instId, refreshSymbolMappingForCopy): - def rewriteFunDefn(fun: FunDefn): RewrittenFunDefn = - fun.params.mapConserve(applyParamList) -> applyBlock(fun.body) + override def rewriteFunDefn(fun: FunDefn): RewrittenFunDefn = + (fun.params.mapConserve(applyParamList), applyBlock(fun.body), fun.annotations) // calls `fun` with possibly remapped `args` def mkCall(fun: Path, args: Ls[LocalVarSymbol])(k: Result => Block): Block = @@ -465,10 +465,10 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo new Rewriter(Nil, N): override def applyFunDefn(fun: FunDefn): FunDefn = rewrittenInPlace.get(fun.dSym) match - case Some((params, rewrittenBody)) => + case Some((params, rewrittenBody, annotations)) => // deforest never changes fun params in place assert(params is fun.params) - FunDefn(fun.owner, fun.sym, fun.dSym, params, rewrittenBody)(fun.configOverride, fun.annotations) + FunDefn(fun.owner, fun.sym, fun.dSym, params, rewrittenBody)(fun.configOverride, annotations) case None => super.applyFunDefn(fun) // ====== end: implements the abstract members of `PolyInstantiationRewrite` ====== end DeforestRewriter diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/EffectAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/EffectAnalysis.scala new file mode 100644 index 0000000000..4328c2d1d4 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/EffectAnalysis.scala @@ -0,0 +1,59 @@ +package hkmc2 +package codegen +package flowAnalysis + +import hkmc2.semantics.* +import hkmc2.utils.*, shorthands.* +import utils.* + + +enum EffectSummary: + case Pure, UnsafeEta + + +object EffectAnalysis: + + def summarize(effectVar: StratVar | NoProd): EffectSummary = + effectVar match + case NoProd => EffectSummary.Pure + case effectVar: StratVar => + if effectVar.lowerBounds.forall(_.isInstanceOf[StratVar]) + then EffectSummary.Pure + else EffectSummary.UnsafeEta + + def logResults(solver: FlowConstraintSolver)(using tl: TraceLogger): Unit = + given fState: FlowAnalysis.State = solver.fState + given raise: Raise = solver.preAnalyzer.raise + given symbolPrinter: SymbolPrinter = solver.preAnalyzer.traceSymbolPrinter + given ShowCfg = ShowCfg.internal + + def showFunction(id: ConcreteId[FunId]): Str = + s"function ${id.showConcreteFunId}" + + def showPath(path: Path): Str = path match + case Value.SimpleRef(sym) => symbolPrinter.printSymbol(sym) + case Value.MemberRef(_, disamb) => symbolPrinter.printSymbol(disamb) + case Select(_, name) => name.name + case _ => "" + + def showCall(id: ConcreteId[CallId]): Str = + val (exprId, whichArgList) = id.exprId + val call = exprId.getResult match + case Call(fun, _) => s"call ${showPath(fun)}@${exprId.showRefSite}" + case Instantiate(_, cls, _) => s"instantiate ${showPath(cls)}@${exprId.showRefSite}" + case other => s"call $other@${exprId.showRefSite}" + s"$call#$whichArgList @ ${id.instId.showInstId}" + + tl.log(">>> effect-analysis results >>>") + solver.functionEffectVars.iterator.map: (id, effect) => + s"${showFunction(id)} -> ${summarize(effect)}" + .toSeq.sorted.foreach(tl.log(_)) + solver.callEffectVars.iterator.map: (id, effect) => + s"${showCall(id)} -> ${summarize(effect)}" + .toSeq.sorted.foreach(tl.log(_)) + tl.log("<<< effect-analysis results <<<") + end logResults + +end EffectAnalysis + + 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 06ea839f99..7c41b8b433 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -16,6 +16,7 @@ object FlowAnalysis: object TraceScope: val NonAffineSyms = "flow-analysis/non-affine" val AccumulatorSym = "flow-analysis/accumulator" + val Effects = "flow-analysis/effects" class State(using val eState: Elaborator.State): val resultToResultId = new java.util.IdentityHashMap[Result, ResultId].asScala @@ -78,11 +79,31 @@ object FlowAnalysis: mono: Bool, nonAffineTracking: Bool, accumulatorTracking: Bool, + logEffects: Bool, )(using TraceLogger, Elaborator.State, Raise, SymbolPrinter) = given State = new State val pre = new FlowPreAnalyzer(pgrm) - val constrCol = new FlowConstraintsCollector(pre, mono, nonAffineTracking, accumulatorTracking) - new FlowConstraintSolver(constrCol) + val constrCol = new FlowConstraintsCollector( + pre, + mono, + nonAffineTracking, + accumulatorTracking, + ) + val solver = new FlowConstraintSolver(constrCol) + val tl = summon[TraceLogger] + if logEffects then tl.scoped(TraceScope.Effects): + if tl.isTracing then EffectAnalysis.logResults(solver) + solver + + // * FIXME: not all symbols have irDefn; add and look up an irHeader instead + private[flowAnalysis] def isPureCall(call: Call, whichArgList: Int): Bool = call.fun match + case Value.SimpleRef(b: BuiltinSymbol) => whichArgList == 0 && b.isPure + case p => p.targetSymbol.exists: + case s: TermSymbol => s.irFunDefn match + case S(fun) => whichArgList < fun.params.size && fun.annotations.exists(_.isInstanceOf[Annot.Pure]) + case N => s.defn.exists: fun => + whichArgList < fun.params.size && fun.annotations.exists(_.isInstanceOf[Annot.Pure]) + case _ => false def mkTraceLogger( cfg: Config.FlowAnalysisConfig, @@ -96,6 +117,7 @@ object FlowAnalysis: protected def forceTrace(scope: Opt[Str]): Bool = scope match case S(TraceScope.NonAffineSyms) => cfg.logNonAffine case S(TraceScope.AccumulatorSym) => cfg.logAccumulator + case S(TraceScope.Effects) => cfg.logEffects case _ => cfg.debug end FlowAnalysis @@ -181,6 +203,9 @@ object FunRef: sealed trait ProdStrat sealed trait ConsStrat +type NoProd = NoProd.type +case object NoProd extends ProdStrat + class StratVar(val name: Str, val sourceSymbol: Opt[Symbol], val generatedForFun: Opt[TermSymbol])(using eState: Elaborator.State) extends Symbol(using eState) with ProdStrat with ConsStrat: def nme: Str = name @@ -239,7 +264,8 @@ class ProdFun( val requiredParamCount: Int, val restParam: Opt[ConsStrat], val res: ProdStrat, - val capturedVarUpperbound: StratVar + val capturedVarUpperbound: StratVar, + val effect: StratVar | NoProd, ) extends ProdStrat with StratWithOrigin[FunId]: def paramInfo: (Int, Int, Bool) = (params.size, requiredParamCount, restParam.isDefined) val dests = MutSet.empty[ConsFun | MarkerConsStrat] @@ -248,6 +274,9 @@ class ProdFun( case object UnknownProd extends MarkerProdStrat +/** A coarse effect marker indicating that the variable is unsafe for eta-epansion. */ +case object UnsafeEta extends MarkerProdStrat + class Ctor( val exprId: ResultId, val instantiationId: Opt[InstantiationId] @@ -262,10 +291,11 @@ class Ctor( class ConsFun( val exprId: CallId, - val instantiationId: Opt[InstantiationId] + val instantiationId: Opt[InstantiationId], )( val params: Ls[ProdStrat], - val res: ConsStrat + val res: ConsStrat, + val effect: StratVar | NoProd, ) extends ConsStrat with StratWithOrigin[CallId]: val srcs = MutSet.empty[ProdFun | MarkerProdStrat] override def toString(): String = @@ -336,7 +366,8 @@ class FlowPreAnalyzer(val pgrm: Program)(using val labelSymToCtxOfLabel = MutMap.empty[Symbol, Ls[InCtx]] val selToCtxOfSel = MutMap.empty[ResultId, Ls[InCtx]] val modSymToBms = MutMap.empty[Symbol, BlockMemberSymbol] - val generatedVars = MutMap.empty[Symbol, StratVar] + val generatedVars = MutMap.empty[Symbol, StratVar] // TODO: tighten `Symbol` type! + val generatedEffects = MutMap.empty[Symbol, StratVar] // TODO: tighten `Symbol` type! // TODO: populate for getters! val capturedVars = MutMap.empty[FunId, LinkedHashSet[Symbol]] val affinityCounts = MutMap.empty[Symbol, Int].withDefaultValue(0) def getEnclosingMatchesForSel(selExprId: ResultId) = @@ -541,7 +572,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using case Define(defn, rest) => applyDefn(defn) applyBlock(rest) - case Throw(exc) => applyResult(exc) + case Throw(exc, _) => applyResult(exc) case Break(label) => () case Continue(label) => () case TryBlock(sub, finallyDo, rest) => @@ -719,8 +750,15 @@ class FlowConstraintsCollector( if cc.forFunGroup.isEmpty then allRealCtors += c c - private val generatedVars: collection.Map[Symbol, StratVar] = + private val generatedVars: collection.Map[Symbol, StratVar] = // TODO: tighten `Symbol` type! preAnalyzer.res.generatedVars.withDefaultValue(preAnalyzer.res.primitiveStratVar) + private val generatedEffects: collection.Map[Symbol, StratVar] = // TODO: tighten `Symbol` type! // TODO: use at access sites of getters! + preAnalyzer.res.generatedEffects.withDefaultValue(preAnalyzer.res.primitiveStratVar) + + // * This is for effect analysis. The nonlocal variables that are potentially shared. + // * If a function accesses a nonlocal, it is conservatively considered unsafe for eta-expansion. + private val nonlocals = preAnalyzer.res.capturedVars.valuesIterator.flatten + .filterNot(_ == eState.runtimeSymbol).toSet locally { val funsToProdStratScheme = MutMap.empty[TermSymbol, ProdStratScheme] @@ -746,14 +784,7 @@ class FlowConstraintsCollector( ConstraintsCollector(Some(groupRep), N).givenIn: cc ?=> for funSym <- groupedFuns do given ConstraintsCollector = cc.forActualRootFun(funSym) - val fun = preAnalyzer.res.funSymToFunDefn(funSym) - val thisFunVar = generatedVars(fun.dSym) - val funProdStrat = mkFunProdStrat( - s"${funSym.nme}_res", - fun.params, - fun.body, - (fun.dSym, -1)) - cc.constrain(funProdStrat, thisFunVar) + processFunctionBody(preAnalyzer.res.funSymToFunDefn(funSym)) if nonAffineTracking then for sym <- preAnalyzer.res.nonAffineSyms @@ -771,7 +802,7 @@ class FlowConstraintsCollector( globalCollector.givenIn: cc ?=> cc.constrain(preAnalyzer.res.primitiveStratVar, UnknownCons) cc.constrain(UnknownProd, preAnalyzer.res.primitiveStratVar) - processBlock(preAnalyzer.pgrm.main)(using cc, UnknownCons) + processBlock(preAnalyzer.pgrm.main)(using cc, UnknownCons, N) // this places non-affine constraints correctly: // - in mono mode there are no per-scc collectors, @@ -817,6 +848,10 @@ class FlowConstraintsCollector( val stratVarMap = MutMap.empty[StratVar, StratVar] def updateInstantiationId(instId: Opt[InstantiationId]) = S(instId.fold(referSite :: Nil)(referSite :: _)) + def duplicateVarStateOpt(s: StratVar | NoProd): StratVar | NoProd = + s match + case v: StratVar => duplicateVarState(v) + case NoProd => NoProd def duplicateVarState(s: StratVar) = if s.generatedForFun.fold(false): forFun => funToSccRep(forFun).fold(false)(_ is groupRep) @@ -830,8 +865,11 @@ class FlowConstraintsCollector( p.requiredParamCount, p.restParam.map(duplicateConsStrat), duplicateProdStrat(p.res), - duplicateVarState(p.capturedVarUpperbound)) + duplicateVarState(p.capturedVarUpperbound), + duplicateVarStateOpt(p.effect)) case UnknownProd => UnknownProd + case UnsafeEta => UnsafeEta + case NoProd => NoProd case c: Ctor => registerCtor( new Ctor(c.exprId, updateInstantiationId(c.instantiationId))( c.ctor, @@ -841,7 +879,8 @@ class FlowConstraintsCollector( case c: ConsFun => new ConsFun(c.exprId, updateInstantiationId(c.instantiationId))( c.params.map(duplicateProdStrat), - duplicateConsStrat(c.res)) + duplicateConsStrat(c.res), + duplicateVarStateOpt(c.effect)) case UnknownCons => UnknownCons case NonAffine => NonAffine case Accumulator => Accumulator @@ -865,9 +904,10 @@ class FlowConstraintsCollector( def mkFunProdStrat( resName: String, - params: Ls[LoweredParamList], + params: NELs[LoweredParamList], body: Block, - funLamId: FunId + funLamId: FunId, + annotations: Ls[Annot] )(using cc: ConstraintsCollector): ProdStrat = def paramListFunId(whichParamList: Int): FunId = funLamId match @@ -877,6 +917,11 @@ class FlowConstraintsCollector( lambdaExprId case other => lastWords(s"unexpected funLamId shape: $other") val res = freshVar(resName, cc.forFun) + val sourceSymbol = funLamId match + case (sym: TermSymbol, _) => S(sym) + case _: ResultId => N + val effect = + freshVar(s"effect_$funLamId", sourceSymbol, cc.forFun) params.foreach: ps => ps.restParam.foreach: p => generatedVars(p.sym).constrainOpaque @@ -887,6 +932,11 @@ class FlowConstraintsCollector( val funValueStrat = params.zipWithIndex.foldRight[ProdStrat](res): case ((ps, whichParamList), acc) => val plFunId = paramListFunId(whichParamList) + // A declared curried function runs its body only at the final list. + // Applying earlier parameter lists only captures arguments; each needs its own, initially pure summary. + val effectParamList = + if whichParamList == params.size - 1 then effect + else freshVar(s"effect_$plFunId", cc.forFun) val capUB = freshVar(s"cap_ub_$plFunId", cc.forFun) for v <- preAnalyzer.res.capturedVars(plFunId) @@ -898,22 +948,35 @@ class FlowConstraintsCollector( ps.paramCountLB, ps.restParam.map(p => generatedVars(p.sym)), acc, - capUB) - processBlock(body)(using cc, res) + capUB, + effectParamList) + // An explicit @pure annotation overrides the latent effect. Keep analyzing body + // calls, but do not let their effects flow into this trusted override. + processBlock(body)(using cc, res, + if annotations.exists(_.isInstanceOf[Annot.Pure]) + then N else S(effect)) funValueStrat def processFunctionDefn(fun: FunDefn)(using cc: ConstraintsCollector): Unit = val isRoot = preAnalyzer.res.rootFunDefns.contains(fun.dSym) if mono || !isRoot then given ConstraintsCollector = if isRoot then cc.forActualRootFun(fun.dSym) else cc + processFunctionBody(fun) + + def processFunctionBody(fun: FunDefn)(using cc: ConstraintsCollector): Unit = + fun.params match + case Nil => + processBlock(fun.body)(using cc, generatedVars(fun.dSym), S(generatedEffects(fun.dSym))) + case nel: NELs[?] => val funProdStrat = mkFunProdStrat( s"${fun.dSym.nme}_res", - fun.params, + nel, fun.body, - (fun.dSym, -1)) + (fun.dSym, -1), + fun.annotations) cc.constrain(funProdStrat, generatedVars(fun.dSym)) - def processClsLikeDefn(cls: ClsLikeDefn)(using cc: ConstraintsCollector): Unit = + def processClsLikeDefn(cls: ClsLikeDefn)(using cc: ConstraintsCollector, currentEffect: Opt[StratVar]): Unit = cls.methodKeys.valuesIterator.foreach(constrainOpaqueResult) cls.privateFields.foreach(sym => generatedVars(sym).constrainOpaque) cls.publicFields.foreach: (_, tsym) => @@ -921,9 +984,9 @@ class FlowConstraintsCollector( cls.methods.foreach: fun => generatedVars(fun.dSym).constrainOpaque fun.params.foreach(_.allParams.foreach(p => generatedVars(p.sym).constrainOpaque)) - processBlock(fun.body)(using cc, UnknownCons) - processBlock(cls.preCtor)(using cc, UnknownCons) - processBlock(cls.ctor)(using cc, UnknownCons) + processBlock(fun.body)(using cc, UnknownCons, N /* methods are always considered impure; we don't track their effects */) + processBlock(cls.preCtor)(using cc, UnknownCons, N /* FIXME: ctor effects should NOT be discarded */) + processBlock(cls.ctor)(using cc, UnknownCons, N /* FIXME: ctor effects should NOT be discarded */) cls.companion.foreach: mod => mod.methodKeys.valuesIterator.foreach(constrainOpaqueResult) mod.privateFields.foreach(sym => generatedVars(sym).constrainOpaque) @@ -931,16 +994,25 @@ class FlowConstraintsCollector( generatedVars(tsym).constrainOpaque mod.methods.foreach: fun => processFunctionDefn(fun) - processBlock(mod.ctor)(using cc, UnknownCons) + processBlock(mod.ctor)(using cc, UnknownCons, N /* FIXME: ctor effects should NOT be discarded */) - def constrainOpaqueResult(r: Result)(using cc: ConstraintsCollector): Unit = + def constrainOpaqueResult(r: Result)(using cc: ConstraintsCollector, currentEffect: Opt[StratVar]): Unit = cc.constrain(processResult(r), UnknownCons) - def processBlock(b: Block)(using cc: ConstraintsCollector, blkRes: ConsStrat): Unit = + def markUnsafeEta()(using cc: ConstraintsCollector, currentEffect: Opt[StratVar]): Unit = + // TODO: don't register such constraints unless a flag is on + currentEffect.foreach(e => cc.constrain(UnsafeEta, e)) + + def processBlock(b: Block)(using cc: ConstraintsCollector, blkRes: ConsStrat, currentEffect: Opt[StratVar]): Unit = val instId = cc.instId b match case Return(res) => cc.constrain(processResult(res), blkRes) - case Throw(exc) => constrainOpaqueResult(exc) + case Throw(exc, annotations) => + if annotations.exists(_.isInstanceOf[Annot.Pure]) then + constrainOpaqueResult(exc)(using cc, N) + else + markUnsafeEta() + constrainOpaqueResult(exc) case Match(scrut, arms, dflt, rest) => val scrutStrat = processResult(scrut) cc.constrain(scrutStrat, new Dtor(scrut.uid, instId)) @@ -961,17 +1033,21 @@ class FlowConstraintsCollector( val rhsStrat = processResult(rhs) lhs.match case NoSymbol => () - case lhs: (LocalVarSymbol | TermSymbol) => cc.constrain(rhsStrat, generatedVars(lhs)) + case lhs: (LocalVarSymbol | TermSymbol) => + if nonlocals.contains(lhs) then markUnsafeEta() + cc.constrain(rhsStrat, generatedVars(lhs)) processBlock(rest) case TryBlock(sub, finallyDo, rest) => processBlock(sub) processBlock(finallyDo) processBlock(rest) case AssignField(lhs, nme, rhs, rest) => + markUnsafeEta() constrainOpaqueResult(lhs) constrainOpaqueResult(rhs) processBlock(rest) case AssignDynField(lhs, fld, kind, rhs, rest) => + markUnsafeEta() constrainOpaqueResult(lhs) constrainOpaqueResult(fld) constrainOpaqueResult(rhs) @@ -982,27 +1058,53 @@ class FlowConstraintsCollector( val rhsStrat = processResult(rhs) cc.constrain(rhsStrat, generatedVars(tsym)) case fun: FunDefn => + // The eta optimizer requires lifting. + // Other definitions are deliberately opaque. + markUnsafeEta() processFunctionDefn(fun) case cls: ClsLikeDefn => + markUnsafeEta() processClsLikeDefn(cls) processBlock(rest) case End(msg) => () case Unreachable(_) => () - def processResult(r: Result)(using cc: ConstraintsCollector): ProdStrat = + def processResult(r: Result)(using cc: ConstraintsCollector, currentEffect: Opt[StratVar]): ProdStrat = val instId = cc.instId + def freshCallEffect(): StratVar = + val effect = freshVar("call_effect", cc.forFun) + // A call's effect contributes to the enclosing function's latent effect. + currentEffect.foreach: enclosingEffect => + cc.constrain(effect, enclosingEffect) + effect + def constrainConstructorEffect(result: Result): Unit = + val unsafeEta = result match + case call: Call => !FlowAnalysis.isPureCall(call, call.argss.size - 1) + case _: Instantiate => true + case _ => false + // Constructor bodies are not modeled here; instantiation may have effects. + if unsafeEta then markUnsafeEta() def handleCallLike(callId: CallId, fStrat: ProdStrat, args: List[Arg]): ProdStrat = val argsStrat = args.map(a => processResult(a.value)) if args.exists(_.spread.isDefined) then + // TODO: preserve callee-effect precision for spread calls + // * Needs to address [todo:effectful-spreads] + markUnsafeEta() cc.constrain(fStrat, UnknownCons) argsStrat.foreach(arg => cc.constrain(arg, UnknownCons)) UnknownProd else val callRes = freshVar("call_res", cc.forFun) - cc.constrain(fStrat, new ConsFun(callId, instId)(argsStrat, callRes)) + val effect = freshCallEffect() + cc.constrain(fStrat, new ConsFun(callId, instId)( + argsStrat, + callRes, + effect, + )) callRes r match case sel@TrackableSelect(from, field, owner) => + markUnsafeEta() val fromStrat = processResult(from) val selRes = freshVar("sel_res", cc.forFun) cc.constrain( @@ -1010,6 +1112,7 @@ class FlowConstraintsCollector( new FieldSel(sel.uid, instId)(field, owner, selRes)) selRes case c@CtorProducer(ctor, args, selectedFrom) if args.forall(_.spread.isEmpty) => + constrainConstructorEffect(c) for qual <- selectedFrom do cc.constrain(processResult(qual), UnknownCons) val argsStrat = args.map: @@ -1033,6 +1136,7 @@ class FlowConstraintsCollector( case tupSize: Int => registerCtor(new Ctor(c.uid, instId)(tupSize, (0 until tupSize).zip(argsStrat).toList)) case c@CtorProducer(_, args, selectedFrom) => + markUnsafeEta() for qual <- selectedFrom do cc.constrain(processResult(qual), UnknownCons) args.foreach(arg => cc.constrain(processResult(arg.value), UnknownCons)) @@ -1042,13 +1146,15 @@ class FlowConstraintsCollector( argss.iterator.zipWithIndex.foldLeft(processResult(fun)): case (fStrat, (args, whichArgList)) => handleCallLike((c.uid, whichArgList), fStrat, args) case i@Instantiate(_, cls, argss) => + constrainConstructorEffect(i) constrainOpaqueResult(cls) argss.flatten.foreach(a => constrainOpaqueResult(a.value)) UnknownProd case lam@Lambda(ps, body) => - mkFunProdStrat("lam_res", ps :: Nil, body, lam.uid) + mkFunProdStrat("lam_res", ps ne_:: Nil, body, lam.uid, Nil) case _: Tuple => lastWords("should be handled in CtorProducer") case Record(_, fields) => + if fields.exists(_.spread) then markUnsafeEta() fields.foreach: case RcdArg(idx, value) => idx.foreach(p => cc.constrain(processResult(p), UnknownCons)) @@ -1056,7 +1162,11 @@ class FlowConstraintsCollector( UnknownProd case p: Path => p match - case refSite@FunRef(f, selectedFrom) => + case refSite @ FunRef(f, selectedFrom) => + // ? [todo:harry-0] + // ? It's a special case for module getters but I'm not sure if + // ? this is good enough. It just conservatively marks it unsafe. + if f.irFunDefn.fold(!f.isAccessPure)(_.params.isEmpty) then markUnsafeEta() refSite match case DynSelect(_, key, DynSelectKind.Member(_)) => cc.constrain(processResult(key), UnknownCons) case SuperSelect(_, base, key) => @@ -1070,26 +1180,53 @@ class FlowConstraintsCollector( fScheme.instantiate(refSite.uid, f) case None => generatedVars(f) case s @ SuperSelect(qual, base, key) => + markUnsafeEta() cc.constrain(processResult(key), UnknownCons) cc.constrain(processResult(qual), UnknownCons) cc.constrain(processResult(base), UnknownCons) generatedVars(s.symbol) - case s@Select(qual, name) => + case s @ Select(qual, name) => + markUnsafeEta() cc.constrain(processResult(qual), UnknownCons) s.symbol.fold(UnknownProd): selSym => generatedVars(selSym) case DynSelect(qual, fld, kind) => + markUnsafeEta() cc.constrain(processResult(qual), UnknownCons) cc.constrain(processResult(fld), UnknownCons) kind match case DynSelectKind.Member(sym) => generatedVars(sym) case DynSelectKind.ArrayIdx | DynSelectKind.Unknown => UnknownProd - case Cast(value, _, _) => + case Cast(value, _, check) => + if check then markUnsafeEta() val valueStrat = processResult(value) cc.constrain(valueStrat, UnknownCons) valueStrat - case Value.MemberRef(_, disamb) => generatedVars(disamb) - case Value.SimpleRef(sym) => generatedVars(sym) + case Value.MemberRef(_, disamb) => + disamb match + case _: ClassSymbol | _: PatternSymbol => + // Just accessing the binding is pure. + () + case sym: TermSymbol => + // (Accessing mutable fields is a Select) + if !sym.isAccessPure then markUnsafeEta() + case _: ModuleOrObjectSymbol => + lastWords("module/object references should be handled by CtorProducer") + case _: TypeAliasSymbol => + lastWords("type aliases should not occur as runtime member references") + case _: TopLevelSymbol => + lastWords("top-level references should use Value.This") + generatedVars(disamb) + case Value.SimpleRef(sym) => + if nonlocals.contains(sym) then + // A special case to avoid regression in deforest/eta-expansion/basic.mls + // To be superseded by our capture analysis / masking. + val isParam = currentEffect.flatMap(_.sourceSymbol).exists: + case fun: TermSymbol => preAnalyzer.res.funSymToFunDefn.get(fun).exists: + _.params.exists(_.paramSyms.contains(sym)) + case _ => false + if !isParam then markUnsafeEta() + generatedVars(sym) case Value.This(_) => UnknownProd case Value.Lit(lit) => UnknownProd } @@ -1108,6 +1245,8 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): val consumersWithSrcs = mutable.Buffer.empty[ConcreteCtorConsumer] val prodFunsWithDests = mutable.Buffer.empty[ProdFun] val consFunsWithSrcs = mutable.Buffer.empty[ConsFun] + val functionEffectVars = LinkedHashMap.empty[ConcreteId[FunId], StratVar | NoProd] + val callEffectVars = LinkedHashMap.empty[ConcreteId[CallId], StratVar | NoProd] private def addCtorDest(c: Ctor, d: ConcreteCtorConsumer | MarkerConsStrat): Unit = if c.dests.isEmpty then ctorsWithDests += c @@ -1199,11 +1338,24 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): def hasConcreteInstantiationId(prodOrCons: ProdStrat | ConsStrat): Boolean = prodOrCons match case s: StratWithOrigin[?] => s.instantiationId.isDefined case _ => true + def registerFunctionEffects(prod: ProdStrat): Unit = prod match + case p: ProdFun => + functionEffectVars(p.concreteId) = p.effect + registerFunctionEffects(p.res) + case _ => () + def inferredCallEffect(c: ConsFun): StratVar | NoProd = + c.exprId.exprId.getResult match + case call: Call if FlowAnalysis.isPureCall(call, c.exprId.whichArgList) => NoProd + case _ => c.effect def handle(constraint: ProdStrat -> ConsStrat): Unit = assert: val (prod, cons) = constraint hasConcreteInstantiationId(prod) && hasConcreteInstantiationId(cons) + registerFunctionEffects(constraint._1) + constraint._2 match + case c: ConsFun => callEffectVars(c.concreteId) = c.effect + case _ => () constraint match case (c: Ctor, d: Dtor) => addCtorDest(c, d) @@ -1252,6 +1404,10 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): case v: StratVar => handle(arg, v.asIntoParam) case _ => () handle(p.res, c.res) + // The callee's latent effect becomes the effect observed at this call. + inferredCallEffect(c) match + case NoProd => () + case ce: StratVar => handle(p.effect, ce) case (p: ProdFun, UnknownCons) => addFunDest(p, UnknownCons) for a <- p.params do handle(UnknownProd, a) @@ -1275,6 +1431,10 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): addFunSrc(c, UnknownProd) for a <- c.params do handle(a, UnknownCons) handle(UnknownProd, c.res) + // The conservative fallback. + inferredCallEffect(c) match + case NoProd => () + case ce: StratVar => handle(UnsafeEta, ce) case (p: StratVar, c: StratVar) => if p.upperBounds.add(c) then for l <- p.lowerBounds do handle(l, c) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index 5b20024ddb..3c45f4b9d2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -897,7 +897,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: else doc" # /* Unreachable: $msg */" case Unreachable(_) => doc"" - case Throw(res) => + case Throw(res, _) => doc" # throw ${result(res)}${mkSemi}" case Break(lbl) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index e0f10ee7bd..f8eb91b4a7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -2778,7 +2778,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui extraInfo = S(sub.showAsTree), )) - case Throw(res) => + case Throw(res, _) => val excWat = result(res) Vector(`throw`(exnTagIdx, Seq(excWat))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index e84016d9e7..0c99cdd4ca 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -556,13 +556,15 @@ sealed trait DefinitionSymbol[Defn <: Definition] extends MemberSymbol: case cd: codegen.ClsLikeDefn => cd /** Whether we know it's pure when selected (eg getters are not always pure). */ - def isPure: Bool = + def isAccessPure: Bool = this match case _: ModuleOrObjectSymbol => true case _ => defn.exists: case d: ClassDef => true + case TermDefinition(k = syntax.MutVal) => false case TermDefinition(k = _: syntax.ValLike) => true + case d @ TermDefinition(k = syntax.Fun, params = Nil) => !d.mayRaiseEffects case TermDefinition(k = syntax.Fun, params = _ :: _) => true // References to functions are only guaranteed to be pure when the functions have parameter lists case _ => false diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 0c6829cba6..f671b2fae7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -39,7 +39,8 @@ enum Annot extends Located: case Generator()(val toLoc: Opt[Loc]) case Async()(val toLoc: Opt[Loc]) case RaiseEffects()(val toLoc: Opt[Loc]) - // Whether the function is guaranteed to not raise effects. + // * EffectAnalysis Annotations + // @pure: overriding annotation indicating that the function is pure. case Pure()(val toLoc: Opt[Loc]) // Buffered classes expose generated allocation and access methods on the class value. case Bufferable(keepInstances: Bool)(val toLoc: Opt[Loc]) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala index e042bb65c2..3501f859ef 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala @@ -320,7 +320,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C private def lowerSplit (split: Split, cont: Result => Block) (using form: IfLikeForm) - (using LoweringCtx) + (using Config, LoweringCtx) : Block = split match case Split.Let(sym, trm, tl) => @@ -426,9 +426,10 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C * Make a block that throws the match error. We might add the information of * match failure in the future. */ - private def throwMatchErrorBlock = + private def throwMatchErrorBlock(using Config) = Throw(Instantiate(mut = false, Select(State.globalThisSymbol.asThis, Tree.Ident("Error"))(S(ctx.builtins.Error), N)(false), - (Value.Lit(syntax.Tree.StrLit("match error"))(N).asArg :: Nil) :: Nil)(InstantiateMetadata.empty, N)) // TODO add failed-match scrutinee info + (Value.Lit(syntax.Tree.StrLit("match error"))(N).asArg :: Nil) :: Nil)(InstantiateMetadata.empty, N), // TODO add failed-match scrutinee info + annotations = if config.aggressive then Annot.Pure()(N) :: Nil else Nil) /** Gives a lowering temp the join of the representations of the results stored into it. * diff --git a/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls b/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls index 24fdc73b33..35bc318a16 100644 --- a/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls +++ b/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls @@ -102,9 +102,12 @@ fun assertModule(module m: ClsMod): module ClsMod = m assertModule(ClsMod).whoami //│ = "module" +// FIXME: It should be a resolver error or so. +// Now the flow analyzer just treat this as an internal error. :breakme +:fixme assertModule(TypMod).whoami -//│ = "module" +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: type aliases should not occur as runtime member references :noInline print(assertModule(TrmMod).whoami) @@ -123,9 +126,12 @@ assertNonModule(ClsMod).whoami //│ = "module" // FIXME: should not resolve to module's `.whoami`, hence should be an error +// FIXME: It should be a resolver error or so. +// Now the flow analyzer just treat this as an internal error. :breakme +:fixme assertNonModule(TypMod).whoami -//│ = "module" +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: type aliases should not occur as runtime member references // FIXME: should not resolve to module's `.whoami`, hence should be an error // FIXME: Should module-check as the term module (or probably we will remove module-checks before that) diff --git a/hkmc2/shared/src/test/mlscript/backlog/ForLLM.mls b/hkmc2/shared/src/test/mlscript/backlog/ForLLM.mls index bfd494a7bb..58bc0e2f8a 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ForLLM.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ForLLM.mls @@ -120,3 +120,17 @@ pattern P = (:radius) => radius //│ ╙── ^^^^^^ // ——— ——— ——— + +// TODO: Calls through one partial application must retain writes to its earlier parameter. +// BlockSimplifier removes the write and repeats counter(0) at each call to next. +:fixme +:noInline +:expect [1, 2] +fun counter(x)() = + set x = x + 1 + x +let next = counter(0) +[next(), next()] +//│ ═══[RUNTIME ERROR] Expected: '[1, 2]', got: '[1, 1]' +//│ = [1, 1] +//│ next = fun diff --git a/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls b/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls index 59dc95bed5..bfffde3d44 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls @@ -59,15 +59,16 @@ case Int then 0 //│ qual = SimpleRef of tmp:runtime⁰ //│ name = Ident of "unreachable" //│ _2 = Return of Lit of IntLit of 0 -//│ dflt = S of Throw of Instantiate: -//│ mut = false -//│ cls = Select{sym=class:Error⁰}: -//│ qual = This of globalThis:globalThis⁰ -//│ name = Ident of "Error" -//│ argss = Ls of -//│ Ls of -//│ Arg: -//│ value = Lit of StrLit of "match error" +//│ dflt = S of Throw: +//│ exc = Instantiate: +//│ mut = false +//│ cls = Select{sym=class:Error⁰}: +//│ qual = This of globalThis:globalThis⁰ +//│ name = Ident of "Error" +//│ argss = Ls of +//│ Ls of +//│ Arg: +//│ value = Lit of StrLit of "match error" //│ rest = Return of MemberRef{disamb=term:lambda¹}: //│ bms = member:lambda⁰ //│ disamb = term:lambda¹ diff --git a/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls index c8dbcfe0d3..36282b0fd1 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls @@ -128,7 +128,6 @@ fun foo(x) = { x.call(), print(42) } #config(effectHandlers: Some(EffectHandlers())) handle h = Object with {} print(h) -//│ //│ > Handler$h$ @@ -145,7 +144,6 @@ foo(h) print(42) #config(liftDefns: None) -//│ //│ > 42 diff --git a/hkmc2/shared/src/test/mlscript/codegen/EffectfulSpreads.mls b/hkmc2/shared/src/test/mlscript/codegen/EffectfulSpreads.mls new file mode 100644 index 0000000000..cad88f29c5 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/EffectfulSpreads.mls @@ -0,0 +1,16 @@ +:. + + +// * [todo:effectful-spreads] +// * From https://github.com/hkust-taco/mlscript/pull/562#discussion_r4216649253 +// * Spreads can be stateful and even have side effects (we can spread arbitrary iterables)... +let xs = mut [1] +fun id(a) = a +fun g(xs) = + let y = id(...xs) + @affine(0) (x) => y + x +let h = g(xs) +set xs.[0] = 2 +h(4) + + diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/basic.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/basic.mls index e0ff410d9c..f178abeab7 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/basic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/basic.mls @@ -51,6 +51,8 @@ c(p(true)) //│ = 1 +// https://github.com/hkust-taco/mlscript/pull/562#discussion_r4320986578 +// TODO: this is a narrow, special-cased fix, to be superceded by capture-analysis :expect O fun p() = X(Y(O)) fun f(x) = if x is diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls index 9d65550750..ab0789f32b 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls @@ -91,10 +91,12 @@ sum(enum(5), 0) // But instead of running an additional block simplifier before the fused pass, it'd be better to enable scoped branches to address this :expect 3 :soir -fun append(xs, ys) = if xs is +:aggressive +// FIXME: Remove these @pure overrides when immutable list-field reads are inferred pure. +@annotations.pure fun append(xs, ys) = if xs is Nil then ys h :: t then h :: append(t, ys) -fun flatten(xss) = if xss is +@annotations.pure fun flatten(xss) = if xss is Nil then Nil h :: t then append(h, flatten(t)) fun len(ls) = if ls is @@ -132,7 +134,7 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set ys = flatten$flatten⁰(arg$Cons$1$); //│ return flatten_append$append$worker⁰(arg$Cons$0$, ys, eta$0$1, eta$0$2, eta$0$4) //│ else -//│ throw new globalThis⁰.Error⁰("match error") +//│ @pure throw new globalThis⁰.Error⁰("match error") //│ end //│ }; //│ @private @@ -147,9 +149,10 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set inlinedVal = flatten_append$append$worker⁰(arg$Cons$1$, ys, undefined, undefined, undefined); //│ return +⁰(1, inlinedVal) //│ else -//│ throw new globalThis⁰.Error⁰("match error") +//│ @pure throw new globalThis⁰.Error⁰("match error") //│ end //│ }; +//│ @pure //│ define append⁰ as fun append¹(xs, ys) { //│ let arg$Cons$0$, arg$Cons$1$, tmp5; //│ match xs @@ -161,9 +164,10 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set tmp5 = append¹(arg$Cons$1$, ys); //│ return Cons⁰(arg$Cons$0$, tmp5) //│ else -//│ throw new globalThis⁰.Error⁰("match error") +//│ @pure throw new globalThis⁰.Error⁰("match error") //│ end //│ }; +//│ @pure //│ define flatten⁰ as fun flatten¹(xss) { //│ let arg$Cons$0$, arg$Cons$1$, tmp5; //│ match xss @@ -175,7 +179,7 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set tmp5 = flatten¹(arg$Cons$1$); //│ return append¹(arg$Cons$0$, tmp5) //│ else -//│ throw new globalThis⁰.Error⁰("match error") +//│ @pure throw new globalThis⁰.Error⁰("match error") //│ end //│ }; //│ define len⁰ as fun len¹(ls) { @@ -188,7 +192,7 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set tmp5 = len¹(arg$Cons$1$); //│ return +⁰(1, tmp5) //│ else -//│ throw new globalThis⁰.Error⁰("match error") +//│ @pure throw new globalThis⁰.Error⁰("match error") //│ end //│ }; //│ set tmp = Cons⁰(2, Nil⁰); diff --git a/hkmc2/shared/src/test/mlscript/effect-analysis/.mls b/hkmc2/shared/src/test/mlscript/effect-analysis/.mls new file mode 100644 index 0000000000..e3f9030862 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/effect-analysis/.mls @@ -0,0 +1 @@ +:.. diff --git a/hkmc2/shared/src/test/mlscript/effect-analysis/Basic.mls b/hkmc2/shared/src/test/mlscript/effect-analysis/Basic.mls new file mode 100644 index 0000000000..8df5e3f1b0 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/effect-analysis/Basic.mls @@ -0,0 +1,272 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefns), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + +open annotations { pure } +declare fun unsafe(): Anything +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + + +// * Pure remains pure through calls +fun leaf_pure(x) = x + 42 +fun node_pure(x) = leaf_pure(x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_pure⁰#0 @ -> Pure +//│ flow-analysis > function node_pure⁰#0 @ -> Pure +//│ flow-analysis > call +⁰@result¹#0 @ -> Pure +//│ flow-analysis > call leaf_pure⁰@result⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * They run in separate diff blocks, so the flow-analysis results of leaf +// * isn't available in node_value. Yet, node_value should be able to infer the +// * purity from the binding type (val) solely. +val leaf_pure = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< +//│ leaf_pure = 42 +//- +fun node_value() = leaf_pure +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_value⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Accessing a pattern is pure +pattern Leaf = 42 +fun node_pattern() = Leaf +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_pattern⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Unsafe propagates through calls; unknown calls are unsafe. +fun leaf_unsafe(x) = unsafe() +fun node_unsafe(x) = leaf_unsafe(x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_unsafe⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > call unsafe@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Unsafe propagates through calls; unknown calls are unsafe. +fun leaf_unsafe(x) = print(42) +fun node_unsafe(x) = leaf_unsafe(x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_unsafe⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > call print@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Propagation through higher-order calls (monomorphic). +fun answer() = 42 +fun leaf_pure_public(x) = x() +private fun leaf_pure_private(x) = x() +fun leaf_unsafe_public(x) = x() +private fun leaf_unsafe_private(x) = x() +fun node() = + leaf_pure_public(answer) + leaf_pure_private(answer) + leaf_unsafe_public(unsafe) + leaf_unsafe_private(unsafe) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function answer⁰#0 @ -> Pure +//│ flow-analysis > function leaf_pure_private⁰#0 @ -> Pure +//│ flow-analysis > function leaf_pure_public⁰#0 @ -> UnsafeEta +//│ flow-analysis > function leaf_unsafe_private⁰#0 @ -> UnsafeEta +//│ flow-analysis > function leaf_unsafe_public⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_pure_private⁰@result²#0 @ -> Pure +//│ flow-analysis > call leaf_pure_public⁰@result³#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_unsafe_private⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_unsafe_public⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call x²@result⁶#0 @ -> Pure +//│ flow-analysis > call x³@result⁷#0 @ -> UnsafeEta +//│ flow-analysis > call x¹@result⁵#0 @ -> UnsafeEta +//│ flow-analysis > call x⁰@result⁴#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + +// * Recursive calls propagate pure and unsafe. +fun node_recursive_pure() = node_recursive_pure() +fun node_recursive_unsafe() = unsafe(); node_recursive_unsafe() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_recursive_pure⁰#0 @ -> Pure +//│ flow-analysis > function node_recursive_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > call ,⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call node_recursive_unsafe⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call unsafe@result²#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * @pure is trusted to be pure. +@pure fun leaf_pure() = unsafe() +fun node_pure() = leaf_pure() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_pure⁰#0 @ -> Pure +//│ flow-analysis > function node_pure⁰#0 @ -> Pure +//│ flow-analysis > call leaf_pure⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call unsafe@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * @pure is trusted to be pure. +@pure fun leaf_pure()() = unsafe() +fun node_pure() = leaf_pure()() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_pure⁰#0 @ -> Pure +//│ flow-analysis > function leaf_pure⁰#1 @ -> Pure +//│ flow-analysis > function node_pure⁰#0 @ -> Pure +//│ flow-analysis > call leaf_pure⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call leaf_pure⁰@result⁰#1 @ -> Pure +//│ flow-analysis > call unsafe@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * @pure is trusted to be pure. +@pure declare fun leaf_pure(): Anything +fun node_pure() = leaf_pure() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_pure⁰#0 @ -> Pure +//│ flow-analysis > call leaf_pure@result⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Methods & Constructors. +// * Methods are unsafe because it's unknwon in compilation time. +// * Constructors are unsafe because it's a limitation of the alaysis implementation. +// TODO: Refine the analysis? +class Box() with + fun pure() = 42 + fun unsafe() = unsafe() +fun box_constructor() = Box() +fun box_instantiation() = new Box() +fun box_pure(box: Box) = box.pure() +fun box_unsafe(box: Box) = box.unsafe() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function box_constructor⁰#0 @ -> UnsafeEta +//│ flow-analysis > function box_instantiation⁰#0 @ -> UnsafeEta +//│ flow-analysis > function box_pure⁰#0 @ -> UnsafeEta +//│ flow-analysis > function box_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > call pure@result²#0 @ -> UnsafeEta +//│ flow-analysis > call unsafe@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call unsafe@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Getters +// TODO: Getters are now always unsafe. +module M with + fun leaf_pure = 42 + fun leaf_unsafe = unsafe() +fun node_pure() = M.leaf_pure +fun node_unsafe() = M.leaf_unsafe +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_pure⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node_unsafe⁰#0 @ -> UnsafeEta +//│ flow-analysis > call unsafe@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Spreading is conservatively unsafe. +@pure +fun leaf_pure_1(...xs) = unsafe() +fun leaf_pure_2(...xs) = 42 +fun node_pure_1(xs) = leaf_pure_1(...xs) +fun node_pure_2(xs) = leaf_pure_2(...xs) +fun node_pure_3(xs) = leaf_pure_1(1, 2, 3) +fun node_pure_4(xs) = leaf_pure_2(1, 2, 3) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_pure_1⁰#0 @ -> Pure +//│ flow-analysis > function leaf_pure_2⁰#0 @ -> Pure +//│ flow-analysis > function node_pure_1⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node_pure_2⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node_pure_3⁰#0 @ -> Pure +//│ flow-analysis > function node_pure_4⁰#0 @ -> Pure +//│ flow-analysis > call leaf_pure_1⁰@result¹#0 @ -> Pure +//│ flow-analysis > call leaf_pure_2⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call unsafe@result²#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// ? See below... +fun leaf_pure() = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_pure⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + +// ? It seems that flow analysis runs per-block separately... +// ? Is that intended? pure() is seen as a unknwon function in leaf_pure. +fun node_pure(x) = leaf_pure() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_pure⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf_pure⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + + +#config(flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: false))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Propagation through higher-order calls (polymorphic). +fun answer() = 42 +fun leaf_pure_public(x) = x() +private fun leaf_pure_private(x) = x() +fun leaf_unsafe_public(x) = x() +private fun leaf_unsafe_private(x) = x() +fun node() = + leaf_pure_public(answer) + leaf_pure_private(answer) + leaf_unsafe_public(unsafe) + leaf_unsafe_private(unsafe) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function answer⁰#0 @ answer³ -> Pure +//│ flow-analysis > function answer⁰#0 @ node⁰.answer² -> Pure +//│ flow-analysis > function answer⁰#0 @ node⁰.answer¹ -> Pure +//│ flow-analysis > function leaf_pure_private⁰#0 @ leaf_pure_private² -> UnsafeEta +//│ flow-analysis > function leaf_pure_private⁰#0 @ node⁰.leaf_pure_private¹ -> Pure +//│ flow-analysis > function leaf_pure_public⁰#0 @ leaf_pure_public² -> UnsafeEta +//│ flow-analysis > function leaf_pure_public⁰#0 @ node⁰.leaf_pure_public¹ -> Pure +//│ flow-analysis > function leaf_unsafe_private⁰#0 @ leaf_unsafe_private² -> UnsafeEta +//│ flow-analysis > function leaf_unsafe_private⁰#0 @ node⁰.leaf_unsafe_private¹ -> UnsafeEta +//│ flow-analysis > function leaf_unsafe_public⁰#0 @ leaf_unsafe_public² -> UnsafeEta +//│ flow-analysis > function leaf_unsafe_public⁰#0 @ node⁰.leaf_unsafe_public¹ -> UnsafeEta +//│ flow-analysis > function node¹#0 @ node⁰ -> UnsafeEta +//│ flow-analysis > call leaf_pure_private⁰@result³#0 @ node⁰ -> Pure +//│ flow-analysis > call leaf_pure_public⁰@result¹#0 @ node⁰ -> Pure +//│ flow-analysis > call leaf_unsafe_private⁰@result⁷#0 @ node⁰ -> UnsafeEta +//│ flow-analysis > call leaf_unsafe_public⁰@result⁵#0 @ node⁰ -> UnsafeEta +//│ flow-analysis > call x²@result⁴#0 @ leaf_unsafe_public² -> UnsafeEta +//│ flow-analysis > call x²@result⁴#0 @ node⁰.leaf_unsafe_public¹ -> UnsafeEta +//│ flow-analysis > call x³@result⁶#0 @ leaf_unsafe_private² -> UnsafeEta +//│ flow-analysis > call x³@result⁶#0 @ node⁰.leaf_unsafe_private¹ -> UnsafeEta +//│ flow-analysis > call x¹@result²#0 @ leaf_pure_private² -> UnsafeEta +//│ flow-analysis > call x¹@result²#0 @ node⁰.leaf_pure_private¹ -> Pure +//│ flow-analysis > call x⁰@result⁰#0 @ leaf_pure_public² -> UnsafeEta +//│ flow-analysis > call x⁰@result⁰#0 @ node⁰.leaf_pure_public¹ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Getters +// TODO: Getters are now always unsafe. +module M with + fun leaf_pure = 42 + fun leaf_unsafe = unsafe() +fun node_pure() = M.leaf_pure +fun node_unsafe() = M.leaf_unsafe +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node_pure⁰#0 @ node_pure¹ -> UnsafeEta +//│ flow-analysis > function node_unsafe⁰#0 @ node_unsafe¹ -> UnsafeEta +//│ flow-analysis > call unsafe@result⁰#0 @ leaf_unsafe⁰ -> UnsafeEta +//│ flow-analysis > call unsafe@result⁰#0 @ node_unsafe¹.leaf_unsafe¹ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< diff --git a/hkmc2/shared/src/test/mlscript/effect-analysis/Imports.mls b/hkmc2/shared/src/test/mlscript/effect-analysis/Imports.mls new file mode 100644 index 0000000000..a3ef085f77 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/effect-analysis/Imports.mls @@ -0,0 +1,75 @@ +:. +// The test harness imports and opens Predef.mls; print's body is outside this program. +:lift +#config(flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true)), inlining: None) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// Isolate print so another effectful call cannot mask a missing imported-call effect. +fun leaf(x) = print(x) +fun node(x) = leaf(x) +fun qualified(x) = Predef.print(x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf⁰#0 @ -> UnsafeEta +//│ flow-analysis > function node⁰#0 @ -> UnsafeEta +//│ flow-analysis > function qualified⁰#0 @ -> UnsafeEta +//│ flow-analysis > call leaf⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call print@result²#0 @ -> UnsafeEta +//│ flow-analysis > call print@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// Passing an imported function as a value must also propagate its unknown effect. +private fun invoke(f, x) = f(x) +fun indirect(x) = invoke(print, x) +fun pure_callback(x) = invoke(y => y, x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function indirect⁰#0 @ -> UnsafeEta +//│ flow-analysis > function invoke⁰#0 @ -> UnsafeEta +//│ flow-analysis > function lambda⁰#0 @ -> Pure +//│ flow-analysis > function pure_callback⁰#0 @ -> UnsafeEta +//│ flow-analysis > call f⁰@result²#0 @ -> UnsafeEta +//│ flow-analysis > call invoke⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call invoke⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +#config(flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: false))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +fun leaf(x) = print(x) +fun node(x) = leaf(x) +fun qualified(x) = Predef.print(x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf⁰#0 @ leaf² -> UnsafeEta +//│ flow-analysis > function leaf⁰#0 @ node⁰.leaf¹ -> UnsafeEta +//│ flow-analysis > function node¹#0 @ node⁰ -> UnsafeEta +//│ flow-analysis > function qualified⁰#0 @ qualified¹ -> UnsafeEta +//│ flow-analysis > call leaf⁰@result²#0 @ node⁰ -> UnsafeEta +//│ flow-analysis > call print@result¹#0 @ leaf² -> UnsafeEta +//│ flow-analysis > call print@result¹#0 @ node⁰.leaf¹ -> UnsafeEta +//│ flow-analysis > call print@result⁰#0 @ qualified¹ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// Poly mode keeps the pure callback separate from the imported print callback. +private fun invoke(f, x) = f(x) +fun indirect(x) = invoke(print, x) +fun pure_callback(x) = invoke(y => y, x) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function indirect¹#0 @ indirect⁰ -> UnsafeEta +//│ flow-analysis > function invoke⁰#0 @ indirect⁰.invoke² -> UnsafeEta +//│ flow-analysis > function invoke⁰#0 @ invoke³ -> UnsafeEta +//│ flow-analysis > function invoke⁰#0 @ pure_callback⁰.invoke¹ -> Pure +//│ flow-analysis > function lambda⁰#0 @ lambda² -> Pure +//│ flow-analysis > function lambda⁰#0 @ pure_callback⁰.lambda¹ -> Pure +//│ flow-analysis > function pure_callback¹#0 @ pure_callback⁰ -> Pure +//│ flow-analysis > call f⁰@result⁰#0 @ indirect⁰.invoke² -> UnsafeEta +//│ flow-analysis > call f⁰@result⁰#0 @ invoke³ -> UnsafeEta +//│ flow-analysis > call f⁰@result⁰#0 @ pure_callback⁰.invoke¹ -> Pure +//│ flow-analysis > call invoke⁰@result²#0 @ indirect⁰ -> UnsafeEta +//│ flow-analysis > call invoke⁰@result¹#0 @ pure_callback⁰ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< diff --git a/hkmc2/shared/src/test/mlscript/effect-analysis/Mutation.mls b/hkmc2/shared/src/test/mlscript/effect-analysis/Mutation.mls new file mode 100644 index 0000000000..3042324751 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/effect-analysis/Mutation.mls @@ -0,0 +1,310 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefn), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + + +// * Mutating a state is an effect. +let state = 0 +fun mutate() = + set state = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 + + +// * Mutating a local (unescaped) variable is pure. +fun mutate_local() = + let local = 0 + set local = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_local⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Reassigning a local copy does not mutate the input binding. +fun mutate_alias(value) = + let local = value + set local = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_alias⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Mutating a local (escaped, captured) variable is an effect. +fun mutate_captured() = + let local = 0 + () => + set local = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function lambda$⁰#0 @ -> Pure +//│ flow-analysis > function lambda$⁰#1 @ -> UnsafeEta +//│ flow-analysis > function lambda⁰#0 @ -> UnsafeEta +//│ flow-analysis > function mutate_captured⁰#0 @ -> UnsafeEta +//│ flow-analysis > call lambda$⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call lambda⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Mutating a state field is an effect. +let state = mut { value: 0 } +fun mutate_field() = + set state.value = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = {value: 0} + +// * Mutating a state element is an effect. +let state = mut [0] +fun mutate_element() = + set state.[0] = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = [0] + + +// * Mutating a local field/element is pure. +// TODO: this shouldn't be an effect because the local is unescaped +// to be refined by a "capture-tracking-based local mutation escape analysis" +fun mutate_local_field() = + let local = mut { value: 0 } + set local.value = 42 +fun mutate_local_element() = + let local = mut [0] + set local.[0] = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_local_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > function mutate_local_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Mutating an aliased field/element is an effect. +fun mutate_alias_field(obj: {value: Int}) = + let local = obj + set local.value = 42 +fun mutate_alias_element(xs) = + let local = xs + set local.[0] = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_alias_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > function mutate_alias_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Observing a state is an effect. +// TODO: this shouldn't be an effect because state is immutable +// to be refined by distinguishing between init and assign +let state = 0 +fun observe() = + state +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 + + +// * Observing a local (unescaped) variable is pure. +fun observe_local() = + let local = 0 + local +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_local⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Observing a local copy of a parameter is pure. +fun observe_alias(value) = + let local = value + local +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_alias⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Observing a local (escaped, captured) variable is pure. +fun observe_captured() = + let local = 0 + () => + local +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function lambda$⁰#0 @ -> Pure +//│ flow-analysis > function lambda$⁰#1 @ -> Pure +//│ flow-analysis > function lambda⁰#0 @ -> Pure +//│ flow-analysis > function observe_captured⁰#0 @ -> Pure +//│ flow-analysis > call lambda$⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call lambda⁰@result¹#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + +// * Observing a mutable state is an effect. +// ? But are there analysis-wise difference between the following 2 cases? +let state = 0 +set state = 42 +fun observe_mutable() = + state +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_mutable⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 42 +let state = 0 +fun observe_mutable() = + state +set state = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_mutable⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 42 + + +// * Observing a state field/element is an effect. +let state = { value: 0 } +fun observe_field() = + state.value +let state = [0] +fun observe_element() = + state.[0] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > function observe_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = [0] + + +// * Observing a local field/element is pure. +// TODO: the same refinement as above +fun observe_local_field() = + let local = { value: 0 } + local.value +fun observe_local_element() = + let local = [0] + local.[0] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_local_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > function observe_local_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Observing an aliased field/element is an effect. +fun observe_alias_field(obj: {value: Int}) = + let local = obj + local.value +fun observe_alias_element(xs) = + let local = xs + local.[0] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function observe_alias_element⁰#0 @ -> UnsafeEta +//│ flow-analysis > function observe_alias_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Some edge cases... +// * Spreading a tuple or record is an effect because it is equivalent to accesses. +// TODO: the same refinement as above +fun tup_spread_local() = + let xs = [42] + [...xs] +fun rec_spread_local() = + let obj = { value: 42 } + {...obj} +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function rec_spread_local⁰#0 @ -> UnsafeEta +//│ flow-analysis > function tup_spread_local⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +let xs = [42] +fun tup_spread_nonlocal() = + [...xs] +let obj = { value: 42 } +fun rec_spread_nonlocal() = + {...obj} +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function rec_spread_nonlocal⁰#0 @ -> UnsafeEta +//│ flow-analysis > function tup_spread_nonlocal⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ obj = {value: 42} +//│ xs = [42] + + +// * Reading a shared capture is an effect even though only the other closure writes it. +fun shared_captures() = + let local = 0 + fun observe() = + local + fun mutate() = + set local = 42 + [observe, mutate] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate$⁰#0 @ -> Pure +//│ flow-analysis > function mutate$⁰#1 @ -> UnsafeEta +//│ flow-analysis > function mutate⁰#0 @ -> UnsafeEta +//│ flow-analysis > function observe$⁰#0 @ -> Pure +//│ flow-analysis > function observe$⁰#1 @ -> UnsafeEta +//│ flow-analysis > function observe⁰#0 @ -> UnsafeEta +//│ flow-analysis > function shared_captures⁰#0 @ -> UnsafeEta +//│ flow-analysis > call mutate$⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call mutate⁰@result²#0 @ -> UnsafeEta +//│ flow-analysis > call observe$⁰@result¹#0 @ -> Pure +//│ flow-analysis > call observe⁰@result³#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Conditional branches retain shared writes. +let state = 0 +fun mutate_if(cond) = + if cond do set state = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_if⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 + + +// * Conditional local mutation remains pure. +fun mutate_local_if(cond) = + let local = 0 + if cond do set local = 42 + local +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_local_if⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Loops retain shared writes. +let state = 0 +fun mutate_loop(again) = + while again do + set state = 42 + set again = false +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_loop⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 + + +// * Local mutation in a loop remains pure. +fun mutate_local_loop(again) = + let local = 0 + while again do + set local = 42 + set again = false + local +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function mutate_local_loop⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< + + +// * [todo:harry-0] A getter from an earlier block is unsafe. +let state = 0 +module M with + fun get = set state = 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 +//- +fun node() = M.get +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< diff --git a/hkmc2/shared/src/test/mlscript/effect-analysis/Regression.mls b/hkmc2/shared/src/test/mlscript/effect-analysis/Regression.mls new file mode 100644 index 0000000000..671c6f313d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/effect-analysis/Regression.mls @@ -0,0 +1,58 @@ +:. +:js +:lift + +#config(deforest: Some(Deforest(debug: false)), flowBasedOpt: Some(FlowBasedOpt(logEffects: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + +data class (::) Cons(h, t) +data object Nil +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// * The unannotated counterpart of deforest/eta-expansion/recursive.mls. +// * Track inferred effects while that test overrides them with @pure. +:expect 3 +:aggressive +fun append(xs, ys) = if xs is + Nil then ys + h :: t then h :: append(t, ys) +fun flatten(xss) = if xss is + Nil then Nil + h :: t then append(h, flatten(t)) +fun len(ls) = if ls is + Nil then 0 + h :: t then 1 + len(t) +len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function append⁰#0 @ -> UnsafeEta +//│ flow-analysis > function flatten$flatten⁰#0 @ -> UnsafeEta +//│ flow-analysis > function flatten_append$append⁰#0 @ -> UnsafeEta +//│ flow-analysis > function flatten⁰#0 @ -> UnsafeEta +//│ flow-analysis > function len$len⁰#0 @ -> Pure +//│ flow-analysis > function len$ls_Cons⁰#0 @ -> Pure +//│ flow-analysis > function len$ls_Cons⁰#1 @ -> Pure +//│ flow-analysis > function len$ls_Nil⁰#0 @ -> Pure +//│ flow-analysis > function len$ls_Nil⁰#1 @ -> Pure +//│ flow-analysis > function len$ls_rest⁰#0 @ -> Pure +//│ flow-analysis > function len⁰#0 @ -> UnsafeEta +//│ flow-analysis > call +⁰@result³#0 @ -> Pure +//│ flow-analysis > call +⁰@result⁸#0 @ -> Pure +//│ flow-analysis > call append⁰@result⁵#0 @ -> UnsafeEta +//│ flow-analysis > call append⁰@result⁷#0 @ -> UnsafeEta +//│ flow-analysis > call flatten$flatten⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call flatten$flatten⁰@result¹⁴#0 @ -> UnsafeEta +//│ flow-analysis > call flatten_append$append⁰@result¹³#0 @ -> UnsafeEta +//│ flow-analysis > call flatten_append$append⁰@result¹¹#0 @ -> UnsafeEta +//│ flow-analysis > call flatten⁰@result⁶#0 @ -> UnsafeEta +//│ flow-analysis > call id@result²#0 @ -> UnsafeEta +//│ flow-analysis > call len$len⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call len$len⁰@result⁹#0 @ -> Pure +//│ flow-analysis > call len$ls_Cons⁰@result¹⁰#0 @ -> Pure +//│ flow-analysis > call len$ls_Nil⁰@result¹⁵#0 @ -> Pure +//│ flow-analysis > call len⁰@result⁴#0 @ -> UnsafeEta +//│ flow-analysis > call ls⁰@result¹²#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< +//│ = 3 diff --git a/hkmc2/shared/src/test/mlscript/opt/Aggressive.mls b/hkmc2/shared/src/test/mlscript/opt/Aggressive.mls new file mode 100644 index 0000000000..937f4bae92 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/opt/Aggressive.mls @@ -0,0 +1,22 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefns), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Match failure is UnsafeEta without aggressive mode. +:noAggressive +fun node(x) = if x is 0 then 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// * Match failure is safe with aggressive mode. +:aggressive +fun node(x) = if x is 0 then 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function node⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< diff --git a/hkmc2/shared/src/test/mlscript/opt/DeadSelRemoval.mls b/hkmc2/shared/src/test/mlscript/opt/DeadSelRemoval.mls index 7facdaa2f6..b7499fb22f 100644 --- a/hkmc2/shared/src/test/mlscript/opt/DeadSelRemoval.mls +++ b/hkmc2/shared/src/test/mlscript/opt/DeadSelRemoval.mls @@ -38,11 +38,12 @@ module Foo with Foo.x //│ = 1 -// * The first selection is pure and can be removed +// * TODO: The first selection is free of side effects and should be dropped, +// * but it is not pure, as it reads mutable state, and we do not currently distinguish the two. :soir Foo.x, Foo.x //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ return Foo⁰.x⁰ +//│ do Foo⁰.x⁰; return Foo⁰.x⁰ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/opt/EtaEffects.mls b/hkmc2/shared/src/test/mlscript/opt/EtaEffects.mls new file mode 100644 index 0000000000..808b311afe --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/opt/EtaEffects.mls @@ -0,0 +1,110 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefns), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, debugEta: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > <<< eta-expansion targets shapes <<< + + +let state = 0 +fun effect() = set state += 42 +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function effect⁰#0 @ -> UnsafeEta +//│ flow-analysis > call +⁰@result⁰#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ state = 0 + + +// * Pure doesn't prevent eta-expansion. +:expect 42 +fun pure() = 42 +@affine(0) fun leaf() = 42 +fun node() = + pure() + leaf +node()() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf⁰#0 @ -> Pure +//│ flow-analysis > function node⁰#0 @ -> Pure +//│ flow-analysis > function pure⁰#0 @ -> Pure +//│ flow-analysis > call baseCall⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call node⁰@result¹#0 @ -> Pure +//│ flow-analysis > call pure⁰@result²#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > node⁰#0 @ : [0, 0] +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ = 42 + + +// * Unsafe prevents eta-expansion. +:expect 42 +@affine(0) fun leaf() = 42 +fun node() = + effect() + leaf +node()() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf⁰#0 @ -> Pure +//│ flow-analysis > function node⁰#0 @ -> UnsafeEta +//│ flow-analysis > call baseCall⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call effect⁰@result²#0 @ -> UnsafeEta +//│ flow-analysis > call node⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ = 42 + + +// * Pure works for expansion across multiple parameter lists +:expect 42 +fun pure() = 42 +@affine(0) fun leaf_2() = + 42 +@affine(0) fun leaf_1() = + pure() + leaf_2 +fun node() = leaf_1 +node()()() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_1⁰#0 @ -> Pure +//│ flow-analysis > function leaf_2⁰#0 @ -> Pure +//│ flow-analysis > function node⁰#0 @ -> Pure +//│ flow-analysis > function pure⁰#0 @ -> Pure +//│ flow-analysis > call baseCall⁰@result¹#0 @ -> Pure +//│ flow-analysis > call callPrefix⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call node⁰@result²#0 @ -> Pure +//│ flow-analysis > call pure⁰@result³#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > node⁰#0 @ : [0, 0, 0] +//│ eta-expansion > leaf_1⁰#0 @ : [0, 0] +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ = 42 + +// * Unsafe prevents direct expansion but still expands as far as possible +:expect 42 +@affine(0) fun leaf_2() = + 42 +@affine(0) fun leaf_1() = + effect() + leaf_2 +fun node() = leaf_1 +node()()() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function leaf_1⁰#0 @ -> UnsafeEta +//│ flow-analysis > function leaf_2⁰#0 @ -> Pure +//│ flow-analysis > function node⁰#0 @ -> Pure +//│ flow-analysis > call baseCall⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call callPrefix⁰@result⁰#0 @ -> Pure +//│ flow-analysis > call effect⁰@result³#0 @ -> UnsafeEta +//│ flow-analysis > call node⁰@result²#0 @ -> Pure +//│ flow-analysis > <<< effect-analysis results <<< +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > node⁰#0 @ : [0, 0] +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ = 42 diff --git a/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls b/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls index c534bdca36..0ba51102bf 100644 --- a/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls +++ b/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls @@ -1,5 +1,6 @@ :. :js +:lift :flowBasedOpt debugEta @@ -281,6 +282,28 @@ app(returnRest2(0)) //│ = [2, 3] +// * It doesn't eta-expand if there's no lifting :) +:expect 42 +#config(liftDefns: None) +@affine(0) fun leaf() = 42 +fun node() = leaf +node()() +//│ dead-param-elim > >>> dead-param-elim results >>> +//│ dead-param-elim > <<< dead-param-elim results <<< +//│ = 42 +:expect 42 +#config(liftDefns: Some(LiftDefns)) +@affine(0) fun leaf() = 42 +fun node() = leaf +node()() +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > node⁰#0 @ : [0, 0] +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ dead-param-elim > >>> dead-param-elim results >>> +//│ dead-param-elim > <<< dead-param-elim results <<< +//│ = 42 + + // a copy that expands a nested `g` must leave the in-place `g` owning its symbol's irDefn: // otherwise the later BlockSimplifier reads the copy's two-param-list `g`, so `g(x)` looks unsaturated, hence pure, and moves past `print` :flowBasedOpt poly @@ -298,23 +321,17 @@ fun f(k, x) = h(2) f(k1, 1) + f(k2, 5) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, f$f; -//│ @private -//│ define f$f as fun f$f⁰(k, x) { -//│ let g; -//│ define g as fun g⁸(y)(eta$0$0) { -//│ let scrut; -//│ set scrut = >⁰(y, 1); -//│ match scrut -//│ true => -//│ do Predef⁰.print⁰("g"); -//│ return k(y)(eta$0$0) -//│ else -//│ return k(y)(eta$0$0) -//│ end -//│ }; -//│ do Predef⁰.print⁰("between"); -//│ return g⁸(x)(2) +//│ let g, tmp, tmp1; +//│ define g as fun g⁸(k, y) { +//│ let scrut; +//│ set scrut = >⁰(y, 1); +//│ match scrut +//│ true => +//│ do Predef⁰.print⁰("g"); +//│ return k(y) +//│ else +//│ return k(y) +//│ end //│ }; //│ @affine(1) //│ define k1⁰ as fun k1¹(a)(b) { @@ -324,23 +341,12 @@ f(k1, 1) + f(k2, 5) //│ return *⁰(a, b) //│ }; //│ define f⁸ as fun f⁹(k, x) { -//│ let g, h; -//│ define g as fun g⁹(y) { -//│ let scrut; -//│ set scrut = >⁰(y, 1); -//│ match scrut -//│ true => -//│ do Predef⁰.print⁰("g"); -//│ return k(y) -//│ else -//│ return k(y) -//│ end -//│ }; -//│ set h = g⁹(x); +//│ let h; +//│ set h = g⁸(k, x); //│ do Predef⁰.print⁰("between"); //│ return h(2) //│ }; -//│ set tmp = f$f⁰(k1¹, 1); +//│ set tmp = f⁹(k1¹, 1); //│ set tmp1 = f⁹(k2¹, 5); //│ return +⁰(tmp, tmp1) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -364,37 +370,38 @@ fun f(k, x) = h(2) f(k1, 1) + f(k2, 5) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, f$f; +//│ let a, b, tmp, tmp1, f_a_b$b, f_a$a, f$f; //│ @private -//│ define f$f as fun f$f¹(k, x) { -//│ let a, b; -//│ define a as fun a⁰(y)(eta$0$0) { -//│ return b⁰(y)(eta$0$0) -//│ }; -//│ define b as fun b⁰(y)(eta$0$0) { -//│ return k(y)(eta$0$0) -//│ }; -//│ return a⁰(x)(2) +//│ define f_a_b$b as fun f_a_b$b⁰(k, y)(eta$0$0) { +//│ return k(y)(eta$0$0) +//│ }; +//│ @private +//│ define f_a$a as fun f_a$a⁰(k, y)(eta$0$0) { +//│ return f_a_b$b⁰(k, y)(eta$0$0) +//│ }; +//│ @private +//│ define f$f as fun f$f⁰(k, x) { +//│ return f_a$a⁰(k, x)(2) +//│ }; +//│ define a as fun a⁰(k, y) { +//│ return b⁰(k, y) +//│ }; +//│ define b as fun b⁰(k, y) { +//│ return k(y) //│ }; //│ @affine(1) -//│ define k1² as fun k1³(a)(b) { -//│ return +⁰(a, b) +//│ define k1² as fun k1³(a1)(b1) { +//│ return +⁰(a1, b1) //│ }; -//│ define k2² as fun k2³(a)(b) { -//│ return *⁰(a, b) +//│ define k2² as fun k2³(a1)(b1) { +//│ return *⁰(a1, b1) //│ }; //│ define f¹⁰ as fun f¹¹(k, x) { -//│ let a, b, h; -//│ define a as fun a¹(y) { -//│ return b¹(y) -//│ }; -//│ define b as fun b¹(y) { -//│ return k(y) -//│ }; -//│ set h = a¹(x); +//│ let h; +//│ set h = a⁰(k, x); //│ return h(2) //│ }; -//│ set tmp = f$f¹(k1³, 1); +//│ set tmp = f$f⁰(k1³, 1); //│ set tmp1 = f¹¹(k2³, 5); //│ return +⁰(tmp, tmp1) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -405,6 +412,7 @@ f(k1, 1) + f(k2, 5) :flowBasedOpt poly :soir :noInline +:w :expect 13 @affine(1) fun k1(a)(b) = a + b fun k2(a)(b) = a * b @@ -415,32 +423,37 @@ fun f(k, x) = let h = g(get(new C(x))) h(2) f(k1, 1) + f(k2, 5) +//│ ═══[WARNING] Cannot yet lift class/module `C` as it is used in an instance check. //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, f$f; +//│ let g, get, tmp, tmp1, f_g$g, f$f; //│ @private -//│ define f$f as fun f$f²(k, x) { -//│ let C, g, get, tmp2, tmp3; -//│ define get as fun get⁰(o) { -//│ match o -//│ C⁰ => -//│ return o.x⁰ -//│ else -//│ throw new globalThis⁰.Error⁰("match error") -//│ end -//│ }; -//│ define g as fun g¹⁰(y)(eta$0$0) { -//│ return k(y)(eta$0$0) -//│ }; -//│ define C as class C⁰ { -//│ val x¹; -//│ constructor C¹(x1) { -//│ define x¹ as val x⁰ = x1; +//│ define f_g$g as fun f_g$g⁰(k, y)(eta$0$0) { +//│ return k(y)(eta$0$0) +//│ }; +//│ @private +//│ define f$f as fun f$f¹(k, x) { +//│ let C, tmp2, tmp3; +//│ define C as class C¹ { +//│ val x⁰; +//│ constructor C⁰(x1) { +//│ define x⁰ as val x¹ = x1; //│ end //│ } //│ }; -//│ set tmp2 = new C⁰(x); -//│ set tmp3 = get⁰(tmp2); -//│ return g¹⁰(tmp3)(2) +//│ set tmp2 = new C¹(x); +//│ set tmp3 = get⁰(C¹, tmp2); +//│ return f_g$g⁰(k, tmp3)(2) +//│ }; +//│ define get as fun get⁰(C$, o) { +//│ match o +//│ C² => +//│ return o.x² +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ }; +//│ define g as fun g⁹(k, y) { +//│ return k(y) //│ }; //│ @affine(1) //│ define k1⁴ as fun k1⁵(a)(b) { @@ -450,18 +463,7 @@ f(k1, 1) + f(k2, 5) //│ return *⁰(a, b) //│ }; //│ define f¹² as fun f¹³(k, x) { -//│ let C, g, get, h, tmp2, tmp3; -//│ define get as fun get¹(o) { -//│ match o -//│ C² => -//│ return o.x² -//│ else -//│ throw new globalThis⁰.Error⁰("match error") -//│ end -//│ }; -//│ define g as fun g¹¹(y) { -//│ return k(y) -//│ }; +//│ let C, h, tmp2, tmp3; //│ define C as class C² { //│ val x³; //│ constructor C³(x1) { @@ -470,15 +472,14 @@ f(k1, 1) + f(k2, 5) //│ } //│ }; //│ set tmp2 = new C²(x); -//│ set tmp3 = get¹(tmp2); -//│ set h = g¹¹(tmp3); +//│ set tmp3 = get⁰(C², tmp2); +//│ set h = g⁹(k, tmp3); //│ return h(2) //│ }; -//│ set tmp = f$f²(k1⁵, 1); +//│ set tmp = f$f¹(k1⁵, 1); //│ set tmp1 = f¹³(k2⁵, 5); //│ return +⁰(tmp, tmp1) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 13 - diff --git a/hkmc2/shared/src/test/mlscript/slop/ConstructorEffects.mls b/hkmc2/shared/src/test/mlscript/slop/ConstructorEffects.mls new file mode 100644 index 0000000000..3e074e4f97 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/ConstructorEffects.mls @@ -0,0 +1,188 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefns), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// Constructor bodies remain available in later compilation blocks. +class Stored(val value: Int) +class Nested(val value: Int) { val stored = Stored(value) } +class MutableField(mut val value: Int) +class Effectful() { print("constructed") } +declare class External() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > call print@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +fun stored(value: Int) = Stored(value) +fun instantiated(value: Int) = new Stored(value) +fun nested(value: Int) = Nested(value) +fun mutable_field(value: Int) = MutableField(value) +fun effectful() = Effectful() +fun external() = new External() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function effectful⁰#0 @ -> UnsafeEta +//│ flow-analysis > function external⁰#0 @ -> UnsafeEta +//│ flow-analysis > function instantiated⁰#0 @ -> UnsafeEta +//│ flow-analysis > function mutable_field⁰#0 @ -> UnsafeEta +//│ flow-analysis > function nested⁰#0 @ -> UnsafeEta +//│ flow-analysis > function stored⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// Constructor summaries must retain effects of calls, writes, and captured reads. +let state = 0 +fun update() = set state += 1 +class Called() { val value = update() } +class Written() { set state += 1 } +class Observed() { val value = state } +class Callback(run: () -> Int) { val value = run() } +fun called() = Called() +fun written() = Written() +fun observed() = Observed() +fun callback(run: () -> Int) = Callback(run) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function callback⁰#0 @ -> UnsafeEta +//│ flow-analysis > function called⁰#0 @ -> UnsafeEta +//│ flow-analysis > function observed⁰#0 @ -> UnsafeEta +//│ flow-analysis > function update⁰#0 @ -> UnsafeEta +//│ flow-analysis > function written⁰#0 @ -> UnsafeEta +//│ flow-analysis > call +⁰@result³#0 @ -> Pure +//│ flow-analysis > call +⁰@result¹#0 @ -> Pure +//│ flow-analysis > call run@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > call update⁰@result²#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ state = 0 + + +// Constructor arguments still execute, even when the constructor body is pure. +declare fun argument_effect(): Int +fun effectful_argument() = Stored(argument_effect()) +fun spread_arguments(args) = new Stored(...args) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function effectful_argument⁰#0 @ -> UnsafeEta +//│ flow-analysis > function spread_arguments⁰#0 @ -> UnsafeEta +//│ flow-analysis > call argument_effect@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// Parent initialization remains conservative until super calls have effect summaries. +class Parent() { print("parent") } +class Child() extends Parent() +fun inherited() = Child() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function inherited⁰#0 @ -> UnsafeEta +//│ flow-analysis > call print@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call super⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// An effectful constructor must execute before its returned closure is called. +:expect [1, 7] +let calls = 0 +class Counted() { + set calls += 1 + val value = 7 +} +@affine(1) fun keep(item: Counted)() = item.value +fun make() = keep(Counted()) +val saved = make() +[calls, saved()] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function keep⁰#0 @ -> Pure +//│ flow-analysis > function keep⁰#1 @ -> UnsafeEta +//│ flow-analysis > function make⁰#0 @ -> UnsafeEta +//│ flow-analysis > call +⁰@result²#0 @ -> Pure +//│ flow-analysis > call keep⁰@result³#0 @ -> Pure +//│ flow-analysis > call make⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call saved⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = [1, 7] +//│ calls = 1 +//│ saved = fun + + +// Reads of captured mutable state cannot move past a subsequent write. +:expect 41 +let current = 41 +class Snapshot() { val value = current } +@affine(1) fun keep_snapshot(item: Snapshot)() = item.value +fun snapshot() = keep_snapshot(Snapshot()) +val saved_snapshot = snapshot() +set current = 42 +saved_snapshot() +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function keep_snapshot⁰#0 @ -> Pure +//│ flow-analysis > function keep_snapshot⁰#1 @ -> UnsafeEta +//│ flow-analysis > function snapshot⁰#0 @ -> UnsafeEta +//│ flow-analysis > call keep_snapshot⁰@result²#0 @ -> Pure +//│ flow-analysis > call saved_snapshot⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > call snapshot⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = 41 +//│ current = 42 +//│ saved_snapshot = fun + + +// Recursive function/constructor dependencies share one effect fixed point. +#config(flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: false))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + +:expect 3 +class Recursive(val value, val n) { + val next = if n > 0 then build(n - 1) else null +} +fun build(n) = Recursive(n, n) +build(3).value +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function build⁰#0 @ build² -> UnsafeEta +//│ flow-analysis > function build⁰#0 @ build³ -> UnsafeEta +//│ flow-analysis > function build⁰#0 @ build¹ -> UnsafeEta +//│ flow-analysis > call -⁰@result²#0 @ -> Pure +//│ flow-analysis > call >⁰@result³#0 @ -> Pure +//│ flow-analysis > call build⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call build⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = 3 + + +let recursive_effects = 0 +class RecursiveEffect(val n) { + set recursive_effects += 1 + val next = if n > 0 then build_effectful(n - 1) else null +} +fun build_effectful(n) = RecursiveEffect(n) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function build_effectful⁰#0 @ build_effectful² -> UnsafeEta +//│ flow-analysis > function build_effectful⁰#0 @ build_effectful¹ -> UnsafeEta +//│ flow-analysis > call +⁰@result³#0 @ -> Pure +//│ flow-analysis > call -⁰@result¹#0 @ -> Pure +//│ flow-analysis > call >⁰@result²#0 @ -> Pure +//│ flow-analysis > call build_effectful⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ recursive_effects = 0 + + +:expect 4 +build_effectful(3) +recursive_effects +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > call build_effectful⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = 4 + + +// Saturated constructor wrappers and direct instantiations share the body summary. +class Curried(val first)(val second) +fun curried(first, second) = Curried(first)(second) +fun new_curried(first, second) = new Curried(first)(second) +fun partial(first) = Curried(first) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function curried⁰#0 @ curried¹ -> UnsafeEta +//│ flow-analysis > function new_curried⁰#0 @ new_curried¹ -> UnsafeEta +//│ flow-analysis > function partial⁰#0 @ partial¹ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< diff --git a/hkmc2/shared/src/test/mlscript/slop/ImmutableFieldEffects.mls b/hkmc2/shared/src/test/mlscript/slop/ImmutableFieldEffects.mls new file mode 100644 index 0000000000..f1abb98d30 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/ImmutableFieldEffects.mls @@ -0,0 +1,74 @@ +:. +:js + +#config(inlining: None, liftDefns: Some(LiftDefns), flowBasedOpt: Some(FlowBasedOpt(logEffects: true, mono: true))) +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +data class Fields(value, mut val changing) +class BodyFields { + val value = 41 + mut val changing = 41 + fun getter = changing +} +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > <<< effect-analysis results <<< + + +// Constructor fields and body fields use different selection paths in flow analysis. +fun read_param(obj: Fields) = obj.value +fun read_mutable_param(obj: Fields) = obj.changing +fun read_body(obj: BodyFields) = obj.value +fun read_mutable_body(obj: BodyFields) = obj.changing +fun read_getter(obj: BodyFields) = obj.getter +fun read_unknown(obj) = obj.("value") +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function read_body⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_getter⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_mutable_body⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_mutable_param⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_param⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_unknown⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// The immutable final field does not hide effects from obtaining its receiver. +class Holder { + mut val current: Fields = Fields(41, 0) + fun getter: Fields = current +} +fun read_mutable_receiver(obj: Holder) = obj.current.value +fun read_getter_receiver(obj: Holder) = obj.getter.value +fun read_called_receiver(get: () -> Fields) = get().value +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > function read_called_receiver⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_getter_receiver⁰#0 @ -> UnsafeEta +//│ flow-analysis > function read_mutable_receiver⁰#0 @ -> UnsafeEta +//│ flow-analysis > call get⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< + + +// Reading a mutable field must observe subsequent writes through the same receiver. +:expect [41, 42] +val fields = Fields(41, 41) +val before = read_mutable_param(fields) +set fields.changing = 42 +[before, read_mutable_param(fields)] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > call read_mutable_param⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call read_mutable_param⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = [41, 42] +//│ before = 41 +//│ fields = Fields(41, 42) + + +:expect [41, 41, 41] +[read_param(fields), read_body(new BodyFields), read_unknown(fields)] +//│ flow-analysis > >>> effect-analysis results >>> +//│ flow-analysis > call read_body⁰@result¹#0 @ -> UnsafeEta +//│ flow-analysis > call read_param⁰@result²#0 @ -> UnsafeEta +//│ flow-analysis > call read_unknown⁰@result⁰#0 @ -> UnsafeEta +//│ flow-analysis > <<< effect-analysis results <<< +//│ = [41, 41, 41] diff --git a/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls b/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls index 4ccd94ad4b..a1c0caa70f 100644 --- a/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls +++ b/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls @@ -1,5 +1,6 @@ :. :flowBasedOpt +:lift :noInline open annotations diff --git a/hkmc2/shared/src/test/mlscript/std/IterTest.mls b/hkmc2/shared/src/test/mlscript/std/IterTest.mls index 8223a935bb..f84010bb30 100644 --- a/hkmc2/shared/src/test/mlscript/std/IterTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/IterTest.mls @@ -95,7 +95,30 @@ mkStr of //│ //│ = "01" -fun xs = [1, 2, 3] Iter.mapping(=> _ + "!" !> print) +let xs = [1, 2, 3] Iter.mapping(=> _ + "!" !> print) +//│ xs = IterableImpl(_) + +// Note: spreads can be effectful! +[...xs] +//│ > 1! +//│ > 2! +//│ > 3! +//│ = ["1!", "2!", "3!"] + +tuple of ...xs +//│ > 1! +//│ > 2! +//│ > 3! +//│ = ["1!", "2!", "3!"] + +// * [todo:effectful-spreads] +// FIXME: doesn't even lower to anything! +:sir +[...xs] +() +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ return runtime⁰.Unit⁰ +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— xs //│ = IterableImpl(_) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 6d08512348..4c10ccb330 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -88,6 +88,8 @@ abstract class MLsDiffMaker extends DiffMaker: val noCheckCasts = NullaryCommand("noCheckCasts") val noFreeze = NullaryCommand("noFreeze") val noModuleCheck = NullaryCommand("noModuleCheck") + val aggressive = NullaryCommand("aggressive") + val noAggressive = NullaryCommand("noAggressive") val effectHandlers = Command("effectHandlers")(_.trim) val effectHandlersOptions = Set("debug", "") val stackSafe = Command("stackSafe")(_.trim) @@ -179,6 +181,7 @@ abstract class MLsDiffMaker extends DiffMaker: commentGeneratedCode = debug.isSet, noFreeze = noFreeze.isSet, noModuleCheck = noModuleCheck.isSet, + aggressive = aggressive.isSet && noAggressive.isUnset, debug = Config.Debug.default, optimizer = Optimizer( deforest = Opt.when(deforest.isSet): @@ -342,6 +345,7 @@ abstract class MLsDiffMaker extends DiffMaker: debugEta = false, debugDpe = false, debugDce = false, + logEffects = false, )) )) processTrees(