Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
bf9e386
Add scoped definition
NeilKleistGao Nov 11, 2025
887ca0b
WIP: Try to get symbols for scoped
NeilKleistGao Nov 14, 2025
148006b
wip
ychenfo Nov 16, 2025
b28875a
wip and some tests
ychenfo Nov 16, 2025
5f9e63c
WIP: Try
NeilKleistGao Nov 17, 2025
cf9d78f
Try to use a pass to insert Scoped
NeilKleistGao Nov 18, 2025
44f132f
Revert "Try to use a pass to insert Scoped"
ychenfo Nov 18, 2025
07eff8f
Revert "WIP: Try"
ychenfo Nov 18, 2025
3108bac
wip:
ychenfo Nov 20, 2025
ab0d660
wip: add known bugs in the tests
ychenfo Nov 20, 2025
79bd913
wip wip
ychenfo Nov 22, 2025
c5d16e0
Revert "wip wip"
ychenfo Nov 23, 2025
69ad890
Revert "wip: add known bugs in the tests"
ychenfo Nov 23, 2025
aeb0c5a
Revert "wip:"
ychenfo Nov 23, 2025
a9cbbf9
Merge remote-tracking branch 'origin/hkmc2' into scope🥨
ychenfo Nov 23, 2025
021c1a0
use collection.Set
ychenfo Nov 23, 2025
0d4ae05
wip: a single if in a nested block..?
ychenfo Nov 23, 2025
f4ccdf5
wip
ychenfo Nov 23, 2025
e796c84
many collection of symbols created during lowering
ychenfo Nov 23, 2025
95c3c12
forgot to collect some symbols and create some scoped blocks
ychenfo Nov 23, 2025
245b9d4
minor
ychenfo Nov 23, 2025
214fc28
minor fix
ychenfo Nov 23, 2025
15f35be
fix two tests
ychenfo Nov 23, 2025
30c1ae9
hopefully complete `Term.definedSyms`
ychenfo Nov 23, 2025
46dd487
wip: make all tests pass
ychenfo Nov 24, 2025
046827a
wip: meaningful use scoped var a bit... now tests related to handler …
ychenfo Nov 24, 2025
12ec16e
wip: nested Scoped blocks for pattern matching bodies with bugs
ychenfo Nov 26, 2025
a11aa2d
Revert "wip: nested Scoped blocks for pattern matching bodies with bugs"
ychenfo Nov 26, 2025
50253ae
wip: try to fix errors in handler tests
ychenfo Nov 27, 2025
1871ea2
wip: smart constructors..?
ychenfo Nov 27, 2025
dde2adf
Revert "wip: try to fix errors in handler tests"
ychenfo Nov 27, 2025
112f419
wip: all tests can pass by not putting scoped blocks for UCS under th…
ychenfo Nov 27, 2025
4aa8c35
update tests
ychenfo Nov 27, 2025
a9cdfd1
jsbuilder: scope.nest.givenIn
ychenfo Nov 27, 2025
7f71ebe
Merge remote-tracking branch 'origin/hkmc2' into scope🥨
ychenfo Nov 27, 2025
2b0590e
WIP: Fix top-level scope & add nest
NeilKleistGao Nov 28, 2025
64385a2
WIP: Try to remove redundant braces (not clean up yet)
NeilKleistGao Nov 29, 2025
36f95ce
WIP: Remove redundant braces (not clean up yet)
NeilKleistGao Nov 29, 2025
0f2f069
WIP: Revert & try to handle non-nested Scoped separately
NeilKleistGao Nov 29, 2025
495956c
more tests; add a fixme; minor
ychenfo Nov 29, 2025
9b5d327
fix incorrect scoped symbols
ychenfo Nov 30, 2025
a64ac1f
get rid of `Term.definedSyms`
ychenfo Dec 1, 2025
ab74823
get rid of vararg
ychenfo Dec 1, 2025
d20dcca
rename
ychenfo Dec 1, 2025
09ea124
fix problems related to handlers in nested `if`/`while`s:
ychenfo Dec 1, 2025
1948288
cleanup
ychenfo Dec 1, 2025
4c7904e
Revert "cleanup"
ychenfo Dec 2, 2025
5ce54b6
Revert "fix problems related to handlers in nested `if`/`while`s:"
ychenfo Dec 2, 2025
10c2ae5
disable while loop rewriting; a temp fix
ychenfo Dec 2, 2025
80fa081
WIP: Add scope.locally
NeilKleistGao Dec 3, 2025
90efc09
Rerun testss
NeilKleistGao Dec 3, 2025
8b3b675
more fixes related to handlers
ychenfo Dec 3, 2025
3dd416f
Merge remote-tracking branch 'cunyuan/scope🥨' into scope🥨
ychenfo Dec 3, 2025
cbe96e0
Minor fix and clean up
NeilKleistGao Dec 4, 2025
e060b40
Remove dontFlatten field of Scoped and remove some other unexpected c…
NeilKleistGao Dec 4, 2025
1ac0ae9
fix scopes for nested if and `floatOutUntilScope`
ychenfo Dec 4, 2025
30f3850
Merge remote-tracking branch 'cunyuan/scope🥨' into scope🥨
ychenfo Dec 4, 2025
d0e2164
Merge remote-tracking branch 'origin/hkmc2' into scope🥨
ychenfo Dec 4, 2025
63fe1c4
try to add some comments to the entangled kludge
ychenfo Dec 4, 2025
43fdc78
update loop rewriting flag
ychenfo Dec 4, 2025
08d4467
some cleanup
ychenfo Dec 4, 2025
9d6b59c
Change the form of scope.locally
NeilKleistGao Dec 5, 2025
9f23fed
Fix the missing case
NeilKleistGao Dec 5, 2025
70ed84a
pr comments
ychenfo Dec 5, 2025
8c11671
cleanup and no "/** scoped **/"
ychenfo Dec 5, 2025
7dc143d
fixes
ychenfo Dec 5, 2025
0227b70
Make some improvements
LPTK Dec 5, 2025
5d99227
Minor fix
NeilKleistGao Dec 6, 2025
a6b1adb
Minor
LPTK Dec 6, 2025
fbc8c13
Merge remote-tracking branch 'origin/hkmc2' into scope🥨
ychenfo Dec 6, 2025
9231b1b
update comments for `definedVarsNoScoped`
ychenfo Dec 6, 2025
899ee24
Add some comments
NeilKleistGao Dec 7, 2025
2482b1e
Update hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala
ychenfo Dec 7, 2025
06aa495
Minor
LPTK Dec 8, 2025
7962e06
Fix flatten for nested scopes
NeilKleistGao Dec 10, 2025
562f927
Changes from meeting
LPTK Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import compiler.ir.Node._

DOCUMENTATION OF SEMANTICS OF @tailcall and @tailrec

FIXME: This doc is a bit outdated, as we have not ported the "modulo-cons" optimization yet.

@tailcall: Used to annotate specific function calls. Calls annotated with @tailcall
must be tail calls or tail modulo-cons calls. These calls must be optimized to not
consume additional stack space. If such an optimization is not possible, then the
Expand Down
12 changes: 11 additions & 1 deletion hkmc2/shared/src/main/scala/hkmc2/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ case class Config(

def stackSafety: Opt[StackSafety] = effectHandlers.flatMap(_.stackSafety)

// NOTE: We force the rewriting of while loops to functions when handler lowering is on
// to prevent the "floating out" of definitions done by handler lowering,
// which currently does not respect scopes introduced by `Scoped` blocks.
// Currently, this is only a problem with while loops because we do not yet
// construct nested Scoped blocks in other places (but will in the future).
// see https://github.com/hkust-taco/mlscript/pull/356#discussion_r2579529893
// and https://github.com/hkust-taco/mlscript/pull/356#discussion_r2585183902
def shouldRewriteWhile: Bool =
rewriteWhileLoops || effectHandlers.isDefined

end Config


Expand All @@ -39,7 +49,7 @@ object Config:
effectHandlers = N,
liftDefns = N,
target = CompilationTarget.JS,
rewriteWhileLoops = true,
rewriteWhileLoops = false,
stageCode = false,
tailRecOpt = true,
)
Expand Down
142 changes: 138 additions & 4 deletions hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,38 @@ sealed abstract class Block extends Product:
case _: End => true
case _ => false

/** This variation of `definedVars` excludes the `syms` in `Scoped` blocks.
* It is used in JSBuilder now: names are allocated in JSBuilder for these `definedVarsNoScoped` symbols.
* This is needed now because
* - there are symbols that are not collected in the `Scoped` blocks (due to some passes that have not yet been adapted to Scoped),
* and they still need to be allocated a name in JSBuilder
* - if we don't exlude the `Scoped` symbols, they may be allocated a name
* prematurely and decalred in wrong places, e.g. symbols inside while bodies
* may be declared in an outer level
*/
lazy val definedVarsNoScoped: Set[Local] = this match
case _: Return | _: Throw => Set.empty
case Begin(sub, rst) => sub.definedVarsNoScoped ++ rst.definedVarsNoScoped
case Assign(l: TermSymbol, r, rst) => rst.definedVarsNoScoped
case Assign(l, r, rst) => rst.definedVarsNoScoped + l
case AssignField(l, n, r, rst) => rst.definedVarsNoScoped
case AssignDynField(l, n, ai, r, rst) => rst.definedVarsNoScoped
case Match(scrut, arms, dflt, rst) =>
arms.flatMap(_._2.definedVarsNoScoped).toSet ++ dflt.toList.flatMap(_.definedVarsNoScoped) ++ rst.definedVarsNoScoped
case End(_) => Set.empty
case Break(_) => Set.empty
case Continue(_) => Set.empty
case Define(defn, rst) =>
val rest = rst.definedVarsNoScoped
if defn.isOwned then rest else rest + defn.sym
// Note that the handler's LHS and body are not part of the current block, so we do not consider them here.
case HandleBlock(lhs, res, par, args, cls, hdr, bod, rst) => rst.definedVarsNoScoped + res
case TryBlock(sub, fin, rst) => sub.definedVarsNoScoped ++ fin.definedVarsNoScoped ++ rst.definedVarsNoScoped
case Label(lbl, _, bod, rst) => bod.definedVarsNoScoped ++ rst.definedVarsNoScoped
case Scoped(syms, body) => body.definedVarsNoScoped -- syms

// * Note: there is a good chance that historical users of `definedVars` do not properly respect Scoped blocks
// * and should adapt their logic to use `definedVarsNoScoped` instead.
lazy val definedVars: Set[Local] = this match
case _: Return | _: Throw => Set.empty
case Begin(sub, rst) => sub.definedVars ++ rst.definedVars
Expand All @@ -49,6 +80,7 @@ sealed abstract class Block extends Product:
case HandleBlock(lhs, res, par, args, cls, hdr, bod, rst) => rst.definedVars + res
case TryBlock(sub, fin, rst) => sub.definedVars ++ fin.definedVars ++ rst.definedVars
case Label(lbl, _, bod, rst) => bod.definedVars ++ rst.definedVars
case Scoped(syms, body) => body.definedVars

lazy val size: Int = this match
case _: Return | _: Throw | _: End | _: Break | _: Continue => 1
Expand All @@ -62,9 +94,11 @@ sealed abstract class Block extends Product:
case TryBlock(sub, fin, rst) => 1 + sub.size + fin.size + rst.size
case Label(_, _, bod, rst) => 1 + bod.size + rst.size
case HandleBlock(lhs, res, par, args, cls, handlers, bdy, rst) => 1 + handlers.map(_.body.size).sum + bdy.size + rst.size
case Scoped(_, body) => body.size

// TODO conserve if no changes
def mapTail(f: BlockTail => Block): Block = this match
case Scoped(syms, body) => Scoped(syms, body.mapTail(f))
case b: BlockTail => f(b)
case Begin(sub, rst) => Begin(sub, rst.mapTail(f))
case Assign(lhs, rhs, rst) => Assign(lhs, rhs, rst.mapTail(f))
Expand Down Expand Up @@ -101,6 +135,7 @@ sealed abstract class Block extends Product:
case Define(defn, rest) => defn.freeVars ++ rest.freeVars
case HandleBlock(lhs, res, par, args, cls, hdr, bod, rst) =>
(bod.freeVars - lhs) ++ rst.freeVars ++ hdr.flatMap(_.freeVars)
case Scoped(syms, body) => body.freeVars
case End(msg) => Set.empty

lazy val freeVarsLLIR: Set[Local] = this match
Expand All @@ -121,6 +156,7 @@ sealed abstract class Block extends Product:
case Define(defn, rest) => defn.freeVarsLLIR ++ (rest.freeVarsLLIR - defn.sym)
case HandleBlock(lhs, res, par, args, cls, hdr, bod, rst) =>
(bod.freeVarsLLIR - lhs) ++ rst.freeVarsLLIR ++ hdr.flatMap(_.freeVarsLLIR)
case Scoped(syms, body) => body.freeVarsLLIR
case End(msg) => Set.empty

lazy val subBlocks: Ls[Block] = this match
Expand All @@ -133,6 +169,7 @@ sealed abstract class Block extends Product:
case Define(d, rest) => d.subBlocks ::: rest :: Nil
case HandleBlock(_, _, par, args, _, handlers, body, rest) => par.subBlocks ++ args.flatMap(_.subBlocks) ++ handlers.map(_.body) :+ body :+ rest
case Label(_, _, body, rest) => body :: rest :: Nil
case Scoped(_, body) => body :: Nil

// TODO rm Lam from values and thus the need for these cases
case Return(r, _) => r.subBlocks
Expand All @@ -147,10 +184,10 @@ sealed abstract class Block extends Product:
// Note that this returns the definitions in reverse order, with the bottommost definiton appearing
// last. This is so that using defns.foldLeft later to add the definitions to the front of a block,
// we don't need to reverse the list again to preserve the order of the definitions.
def floatOutDefns(
ignore: Defn => Bool = _ => false,
preserve: Defn => Bool = _ => false
) =
def extractDefns(
ignore: Defn => Bool = _ => false,
preserve: Defn => Bool = _ => false
): (Block, List[Defn]) =
var defns: List[Defn] = Nil
val transformer = new BlockTransformerShallow(SymbolSubst()):
override def applyBlock(b: Block): Block = b match
Expand All @@ -164,6 +201,12 @@ sealed abstract class Block extends Product:

(transformer.applyBlock(this), defns)

def gatherDefns(
ignore: Defn => Bool = _ => false,
preserve: Defn => Bool = _ => false
): List[Defn] = extractDefns(ignore, preserve)._2 // TODO: fix this very inefficient implementation


lazy val flattened: Block = this.flatten(identity)

private def flatten(k: End => Block): Block = this match
Expand All @@ -183,6 +226,13 @@ sealed abstract class Block extends Product:
if (newBody is body) && (newRest is rest)
then this
else Label(label, loop, newBody, newRest)

// * Do not omit the `Begin`s that are used for nested scopes
case Begin(e: End, Scoped(syms, body)) =>
val newBody = body.flatten(k)
if newBody is body
then this
else new Begin(e, new Scoped(syms, newBody))

case Begin(sub, rest) =>
sub.flatten(_ => rest.flatten(k))
Expand Down Expand Up @@ -247,6 +297,12 @@ sealed abstract class Block extends Product:
then this
else HandleBlock(lhs, res, par, args, cls, newHandlers, newBody, newRest)

case Scoped(syms, body) =>
val newBody = body.flatten(k)
if newBody is body
then this
else Scoped(syms, newBody)

case e: End => k(e)
case t: BlockTail => this

Expand All @@ -272,6 +328,9 @@ case class Label(label: Local, loop: Bool, body: Block, rest: Block) extends Blo
case class Break(label: Local) extends BlockTail
case class Continue(label: Local) extends BlockTail


case class Scoped(syms: collection.Set[Local], body: Block) extends BlockTail

// TODO: remove this form?
case class Begin(sub: Block, rest: Block) extends Block with ProductWithTail

Expand All @@ -286,6 +345,47 @@ case class AssignDynField(lhs: Path, fld: Path, arrayIdx: Bool, rhs: Result, res

case class Define(defn: Defn, rest: Block) extends Block with ProductWithTail

object Match:
def apply(scrut: Path, arms: Ls[Case -> Block], dflt: Opt[Block], rest: Block): Block = rest match
case Scoped(syms, body) => Scoped(syms, Match(scrut, arms, dflt, body))
case _ => new Match(scrut, arms, dflt, rest)
object Label:
def apply(label: Local, loop: Bool, body: Block, rest: Block): Block = rest match
case Scoped(syms, rest) => Scoped(syms, Label(label, loop, body, rest))
case _ => new Label(label, loop, body, rest)
object Scoped:
def apply(syms: collection.Set[Local], body: Block): Block = body match
case Scoped(syms2, body) =>
if syms2.isEmpty && syms.isEmpty then Scoped(Set.empty, body) else Scoped(syms ++ syms2, body)
case _ =>
if syms.isEmpty then body else new Scoped(syms, body)
object Begin:
def apply(sub: Block, rest: Block): Block = (sub, rest) match
case (Scoped(symsSub, bodySub), Scoped(symsRest, bodyRest)) =>
Scoped(symsSub ++ symsRest, Begin(bodySub, bodyRest))
case (Scoped(symsSub, bodySub), _) => Scoped(symsSub, Begin(bodySub, rest))
case (_, Scoped(symsRest, bodyRest)) => Scoped(symsRest, Begin(sub, bodyRest))
case _ => new Begin(sub, rest)
object TryBlock:
def apply(sub: Block, finallyDo: Block, rest: Block): Block = rest match
case Scoped(syms, body) => Scoped(syms, TryBlock(sub, finallyDo, body))
case _ => new TryBlock(sub, finallyDo, rest)
object Assign:
def apply(lhs: Local, rhs: Result, rest: Block): Block = rest match
case Scoped(syms, body) => Scoped(syms, Assign(lhs, rhs, body))
case _ => new Assign(lhs, rhs, rest)
object AssignField:
def apply(lhs: Path, nme: Tree.Ident, rhs: Result, rest: Block)(symbol: Opt[MemberSymbol]): Block = rest match
case Scoped(syms, body) => Scoped(syms, AssignField(lhs, nme, rhs, body)(symbol))
case _ => new AssignField(lhs, nme, rhs, rest)(symbol)
object AssignDynField:
def apply(lhs: Path, fld: Path, arrayIdx: Bool, rhs: Result, rest: Block): Block = rest match
case Scoped(syms, body) => Scoped(syms, AssignDynField(lhs, fld, arrayIdx, rhs, body))
case _ => new AssignDynField(lhs, fld, arrayIdx, rhs, rest)
object Define:
def apply(defn: Defn, rest: Block): Block = rest match
case Scoped(syms, body) => Scoped(syms, Define(defn, body))
case _ => new Define(defn, rest)

case class HandleBlock(
lhs: Local,
Expand All @@ -298,6 +398,40 @@ case class HandleBlock(
rest: Block
) extends Block with ProductWithTail

object HandleBlock:
def apply(
lhs: Local,
res: Local,
par: Path,
args: Ls[Path],
cls: ClassSymbol,
handlers: Ls[Handler],
body: Block,
rest: Block
) = rest match
case Scoped(syms, rest) =>
Scoped(
syms,
new HandleBlock(
lhs,
res,
par,
args,
cls,
handlers,
body,
rest
))
case _ => new HandleBlock(
lhs,
res,
par,
args,
cls,
handlers,
body,
rest)


sealed abstract class Defn:
val innerSym: Opt[MemberSymbol]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ class BlockTransformer(subst: SymbolSubst):
if (lhs2 is lhs) && (fld2 is fld) && (rhs2 is rhs) && (rest2 is rest)
then b
else AssignDynField(lhs2, fld2, arrayIdx, rhs2, rest2)
case Scoped(s, bd) =>
val nb = applySubBlock(bd)
if nb is bd then b else Scoped(s, nb)

def applyRcdArg(rcdArg: RcdArg)(k: RcdArg => Block): Block =
val RcdArg(idx, p) = rcdArg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class BlockTraverser:
applyResult(rhs)
applyPath(fld)
applySubBlock(rest)
case Scoped(_, body) => applySubBlock(body)

def applyResult(r: Result): Unit = r match
case r @ Call(fun, args) => applyPath(fun); args.foreach(applyArg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise,
// ignored cases
case TryBlock(sub, finallyDo, rest) => ??? // ignore
case Throw(_) => PartRet(blk, Nil)
case Scoped(_, body) => go(body)
case _: HandleBlock => lastWords("unexpected handleBlock") // already translated at this point

val PartRet(head, states) = go(blk)(using labelIds, N)
Expand Down Expand Up @@ -581,7 +582,7 @@ class HandlerLowering(paths: HandlerPaths, opt: EffectHandlers)(using TL, Raise,
private def thirdPass(b: Block): Block =
// to ensure the fun and class references in the continuation class are properly scoped,
// we move all function defns to the top level of the handler block
val (blk, defns) = b.floatOutDefns()
val (blk, defns) = b.extractDefns()
defns.foldLeft(blk)((acc, defn) => Define(defn, acc))

private def locToStr(l: Loc): Str =
Expand Down
8 changes: 5 additions & 3 deletions hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,10 @@ class Lifter(handlerPaths: Opt[HandlerPaths])(using State, Raise):

extension (b: Block)
private def floatOut(ctx: LifterCtx) =
b.floatOutDefns(preserve = defn => ctx.isModOrObj(defn.sym) || ctx.ignored(defn.sym))

b.extractDefns(preserve = defn => ctx.isModOrObj(defn.sym) || ctx.ignored(defn.sym))
private def gather(ctx: LifterCtx) =
b.gatherDefns(preserve = defn => ctx.isModOrObj(defn.sym) || ctx.ignored(defn.sym))


def createLiftInfoCont(d: Defn, parentCls: Opt[ClsLikeDefn], ctx: LifterCtx): Map[BlockMemberSymbol, LiftedInfo] =
val AccessInfo(accessed, _, refdDefns) = ctx.getAccesses(d.sym)
Expand Down Expand Up @@ -600,7 +602,7 @@ class Lifter(handlerPaths: Opt[HandlerPaths])(using State, Raise):
defns.flatMap(createLiftInfoCont(_, N, ctx.addFnLocals(ctx.usedLocals(f.sym)))).toMap

def createLiftInfoCls(c: ClsLikeDefn, ctx: LifterCtx): Map[BlockMemberSymbol, LiftedInfo] =
val defns = c.preCtor.floatOut(ctx)._2 ++ c.ctor.floatOut(ctx)._2 ++ c.companion.fold(Nil)(_.ctor.floatOut(ctx)._2)
val defns = c.preCtor.gather(ctx) ++ c.ctor.gather(ctx) ++ c.companion.fold(Nil)(_.ctor.gather(ctx))
val newCtx = if (c.companion.isDefined) && !ctx.ignored(c.sym) then ctx else ctx.addClsDefn(c)
val staticMtdInfo = c.companion.fold(Map.empty):
case value => value.methods.flatMap(f => createLiftInfoFn(f, newCtx))
Expand Down
Loading
Loading