Skip to content

Commit df1adc5

Browse files
authored
Make local continuations direct style if possible (#777)
This PR tries to remove the overhead of CPS (and trampolining) for direct style code. Essentially, for the following code in CPS ``` let cont k(x, ks) = ... if (cond) { k(42, ks') } else { k(43, ks') } ``` we are now generating the following JS: ```javascript let x; if (cond) { x = 42 } else { x = 43 }; ... ``` This only works under the assumption that: - `k` is not used somewhere in a more first-class manner (under a different context, as part of a continuation passed to a function, etc.) - `k` is always called with the currently in scope meta continuation `ks'` (so that we can avoid assigning it -- this turns out to be essential with the feature interaction of loops) The interaction with loops was difficult to get right: - turning recursive functions into direct style loops makes additional continuations more local - making a continuation more local turns more functions into loops As a result, the function `parse_worker` (from the `parsing_dollars` benchmark) is now a tight loop. **Before the PR** ```javascript function parse_worker_0(a_1, ks_7, k_4) { const x_0 = i_0.value; function k_3(c_0, ks_4) { if (c_0 === (36)) { return () => parse_worker_0((a_1 + (1)), ks_4, k_4); } else if (c_0 === (10)) { const x_1 = s_0.value; s_0.value = (x_1 + a_1); return () => parse_worker_0(0, ks_4, k_4); } else { return SHIFT(p_0, (k_5, ks_5, k_6) => k_6($effekt.unit, ks_5), ks_4, k_4); } } if ((x_0 > n_0)) { return SHIFT(p_0, (k_7, ks_6, k_8) => k_8($effekt.unit, ks_6), ks_7, (v_r_1, ks_8) => $effekt.emptyMatch()); } else { const x_2 = j_0.value; if (x_2 === (0)) { const x_3 = i_0.value; i_0.value = (x_3 + (1)); const x_4 = i_0.value; j_0.value = x_4; return () => k_3(10, ks_7); } else { const x_5 = j_0.value; j_0.value = (x_5 - (1)); return () => k_3(36, ks_7); } } } ``` **After the PR** ```javascript function parse_worker_0(a_9, ks_293, k_236) { parse_worker_0: while (true) { const x_11 = i_13.value; let c_0 = undefined; if ((x_11 > n_17)) { return SHIFT(p_32, (k_232, ks_292, k_233) => k_233($effekt.unit, ks_292), ks_293, (v_r_95, ks_294) => $effekt.emptyMatch()); } else { const x_12 = j_0.value; if (x_12 === (0)) { const x_13 = i_13.value; i_13.value = (x_13 + (1)); const x_14 = i_13.value; j_0.value = x_14; c_0 = 10; } else { const x_15 = j_0.value; j_0.value = (x_15 - (1)); c_0 = 36; } } if (c_0 === (36)) { /* prepare tail call */ const a_8 = (a_9 + (1)); a_9 = a_8; continue parse_worker_0; } else if (c_0 === (10)) { const x_16 = s_4.value; s_4.value = (x_16 + a_9); /* prepare tail call */ const a_10 = 0; a_9 = a_10; continue parse_worker_0; } else { return SHIFT(p_32, (k_234, ks_295, k_235) => k_235($effekt.unit, ks_295), ks_293, k_236); } } } ``` Additionally, here are a few preliminary benchmark results: ![image](https://github.com/user-attachments/assets/6d3bd21a-fdcf-4149-8e0a-78ba48c4c6d2)
1 parent 957517a commit df1adc5

File tree

2 files changed

+170
-45
lines changed

2 files changed

+170
-45
lines changed

effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,39 @@ object Normalizer { normal =>
209209

210210
case Stmt.Val(id, tpe, binding, body) =>
211211

212-
def normalizeVal(id: Id, tpe: ValueType, binding: Stmt, body: Stmt): Stmt = binding match {
212+
// def barendregt(stmt: Stmt): Stmt = new Renamer().apply(stmt)
213+
214+
def normalizeVal(id: Id, tpe: ValueType, binding: Stmt, body: Stmt): Stmt = normalize(binding) match {
213215

214216
// [[ val x = sc match { case id(ps) => body2 }; body ]] = sc match { case id(ps) => val x = body2; body }
215217
case Stmt.Match(sc, List((id2, BlockLit(tparams2, cparams2, vparams2, bparams2, body2))), None) =>
216218
Stmt.Match(sc, List((id2, BlockLit(tparams2, cparams2, vparams2, bparams2,
217219
normalizeVal(id, tpe, body2, body)))), None)
218220

221+
// These rewrites do not seem to contribute a lot given their complexity...
222+
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
223+
224+
// [[ val x = if (cond) { thn } else { els }; body ]] = if (cond) { [[ val x = thn; body ]] } else { [[ val x = els; body ]] }
225+
// case normalized @ Stmt.If(cond, thn, els) if body.size <= 2 =>
226+
// // since we duplicate the body, we need to freshen the names
227+
// val normalizedThn = barendregt(normalizeVal(id, tpe, thn, body))
228+
// val normalizedEls = barendregt(normalizeVal(id, tpe, els, body))
229+
//
230+
// Stmt.If(cond, normalizedThn, normalizedEls)
231+
//
232+
// case Stmt.Match(sc, clauses, default)
233+
// // necessary since otherwise we loose Nothing-boxing
234+
// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
235+
// if body.size <= 2 && (clauses.size + default.size) >= 1 =>
236+
// val normalizedClauses = clauses map {
237+
// case (id2, BlockLit(tparams2, cparams2, vparams2, bparams2, body2)) =>
238+
// (id2, BlockLit(tparams2, cparams2, vparams2, bparams2, barendregt(normalizeVal(id, tpe, body2, body))): BlockLit)
239+
// }
240+
// val normalizedDefault = default map { stmt => barendregt(normalizeVal(id, tpe, stmt, body)) }
241+
// Stmt.Match(sc, normalizedClauses, normalizedDefault)
242+
243+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
244+
219245
// [[ val x = return e; s ]] = let x = [[ e ]]; [[ s ]]
220246
case Stmt.Return(expr2) =>
221247
Stmt.Let(id, tpe, expr2, normalize(body)(using C.bind(id, expr2)))
@@ -245,7 +271,7 @@ object Normalizer { normal =>
245271
case normalizedBody => Stmt.Val(id, tpe, other, normalizedBody)
246272
}
247273
}
248-
normalizeVal(id, tpe, normalize(binding), body)
274+
normalizeVal(id, tpe, binding, body)
249275

250276

251277
// "Congruences"

effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala

Lines changed: 142 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import effekt.context.Context
66
import effekt.context.assertions.*
77
import effekt.cps.*
88
import effekt.core.{ DeclarationContext, Id }
9+
import effekt.cps.Variables.{ all, free }
910

1011
import scala.collection.mutable
1112

@@ -21,45 +22,31 @@ object TransformerCps extends Transformer {
2122
val DEALLOC = Variable(JSName("DEALLOC"))
2223
val TRAMPOLINE = Variable(JSName("TRAMPOLINE"))
2324

24-
class Used(var used: Boolean)
25-
case class DefInfo(id: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, used: Used)
26-
27-
object DefInfo {
28-
def unapply(b: cps.Block)(using C: TransformerContext): Option[(Id, List[Id], List[Id], Id, Id, Used)] = b match {
29-
case cps.Block.BlockVar(id) => C.definitions.get(id) match {
30-
case Some(DefInfo(id, vparams, bparams, ks, k, used)) => Some((id, vparams, bparams, ks, k, used))
31-
case None => None
32-
}
33-
case _ => None
34-
}
35-
}
25+
class RecursiveUsage(var jumped: Boolean)
26+
case class RecursiveDefInfo(id: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, used: RecursiveUsage)
27+
case class ContinuationInfo(k: Id, param: Id, ks: Id)
3628

3729
case class TransformerContext(
3830
requiresThunk: Boolean,
31+
// known definitions of expressions (used to inline into externs)
3932
bindings: Map[Id, js.Expr],
4033
// definitions of externs (used to inline them)
4134
externs: Map[Id, cps.Extern.Def],
42-
// currently, lexically enclosing functions and their parameters (used to determine whether a call is recursive, to rewrite into a loop)
43-
definitions: Map[Id, DefInfo],
35+
// the innermost (in direct style) enclosing functions (used to rewrite a definition to a loop)
36+
recursive: Option[RecursiveDefInfo],
37+
// the direct-style continuation, if available (used in case cps.Stmt.LetCont)
38+
directStyle: Option[ContinuationInfo],
39+
// the current direct-style metacontinuation
40+
metacont: Option[Id],
41+
// substitutions for renaming of metaconts (to avoid rebinding them)
42+
metaconts: Map[Id, Id],
4443
// the original declaration context (used to compile pattern matching)
4544
declarations: DeclarationContext,
4645
// the usual compiler context
4746
errors: Context
4847
)
4948
implicit def autoContext(using C: TransformerContext): Context = C.errors
5049

51-
def lookup(id: Id)(using C: TransformerContext): js.Expr = C.bindings.getOrElse(id, nameRef(id))
52-
53-
def enterDefinition(id: Id, used: Used, block: cps.Block)(using C: TransformerContext): TransformerContext = block match {
54-
case cps.BlockLit(vparams, bparams, ks, k, body) =>
55-
C.copy(definitions = Map(id -> DefInfo(id, vparams, bparams, ks, k, used)))
56-
case _ => C
57-
}
58-
59-
def clearDefinitions(using C: TransformerContext): TransformerContext = C.copy(definitions = Map.empty)
60-
61-
def bindingAll[R](bs: List[(Id, js.Expr)])(body: TransformerContext ?=> R)(using C: TransformerContext): R =
62-
body(using C.copy(bindings = C.bindings ++ bs))
6350

6451
/**
6552
* Entrypoint used by the compiler to compile whole programs
@@ -78,6 +65,9 @@ object TransformerCps extends Transformer {
7865
false,
7966
Map.empty,
8067
externs.collect { case d: Extern.Def => (d.id, d) }.toMap,
68+
None,
69+
None,
70+
None,
8171
Map.empty,
8272
D, C)
8373

@@ -100,6 +90,9 @@ object TransformerCps extends Transformer {
10090
false,
10191
Map.empty,
10292
input.externs.collect { case d: Extern.Def => (d.id, d) }.toMap,
93+
None,
94+
None,
95+
None,
10396
Map.empty,
10497
D, C)
10598

@@ -158,18 +151,18 @@ object TransformerCps extends Transformer {
158151

159152
def toJS(id: Id, b: cps.Block)(using TransformerContext): js.Expr = b match {
160153
case cps.Block.BlockLit(vparams, bparams, ks, k, body) =>
161-
val used = new Used(false)
154+
val used = new RecursiveUsage(false)
162155

163-
val translatedBody = toJS(body)(using enterDefinition(id, used, b)).stmts
156+
val translatedBody = toJS(body)(using recursive(id, used, b)).stmts
164157

165-
if used.used then
158+
if used.jumped then
166159
js.Lambda(vparams.map(nameDef) ++ bparams.map(nameDef) ++ List(nameDef(ks), nameDef(k)),
167160
List(js.While(RawExpr("true"), translatedBody, Some(uniqueName(id)))))
168161
else
169162
js.Lambda(vparams.map(nameDef) ++ bparams.map(nameDef) ++ List(nameDef(ks), nameDef(k)),
170163
translatedBody)
171164

172-
case other => toJS(other)(using clearDefinitions)
165+
case other => toJS(other)
173166
}
174167

175168
def toJS(b: cps.Block)(using TransformerContext): js.Expr = b match {
@@ -185,17 +178,18 @@ object TransformerCps extends Transformer {
185178
case cps.Implementation(interface, operations) =>
186179
js.Object(operations.map {
187180
case cps.Operation(id, vps, bps, ks, k, body) =>
188-
nameDef(id) -> js.Lambda(vps.map(nameDef) ++ bps.map(nameDef) ++ List(nameDef(ks), nameDef(k)), toJS(body)(using clearDefinitions).stmts)
181+
nameDef(id) -> js.Lambda(vps.map(nameDef) ++ bps.map(nameDef) ++ List(nameDef(ks), nameDef(k)), toJS(body)(using nonrecursive(ks)).stmts)
189182
})
190183
}
191184

192-
def toJS(ks: cps.MetaCont): js.Expr = nameRef(ks.id)
185+
def toJS(ks: cps.MetaCont)(using T: TransformerContext): js.Expr =
186+
nameRef(T.metaconts.getOrElse(ks.id, ks.id))
193187

194188
def toJS(k: cps.Cont)(using T: TransformerContext): js.Expr = k match {
195189
case Cont.ContVar(id) =>
196190
nameRef(id)
197191
case Cont.ContLam(result, ks, body) =>
198-
js.Lambda(List(nameDef(result), nameDef(ks)), toJS(body)(using clearDefinitions).stmts)
192+
js.Lambda(List(nameDef(result), nameDef(ks)), toJS(body)(using nonrecursive(ks)).stmts)
199193
}
200194

201195
def toJS(e: cps.Expr)(using D: TransformerContext): js.Expr = e match {
@@ -225,9 +219,18 @@ object TransformerCps extends Transformer {
225219
js.Const(nameDef(id), toJS(binding)) :: toJS(body).run(k)
226220
}
227221

228-
case cps.Stmt.LetCont(id, binding, body) =>
222+
// [[ let k(x, ks) = ...; if (...) jump k(42, ks2) else jump k(10, ks3) ]] =
223+
// let x; if (...) { x = 42; ks = ks2 } else { x = 10; ks = ks3 } ...
224+
case cps.Stmt.LetCont(id, Cont.ContLam(param, ks, body), body2) if canBeDirect(id, body2) =>
229225
Binding { k =>
230-
js.Const(nameDef(id), toJS(binding)) :: requiringThunk { toJS(body)(using clearDefinitions) }.run(k)
226+
js.Let(nameDef(param), js.Undefined) ::
227+
toJS(body2)(using withDirectStyle(id, param, ks)).stmts ++
228+
toJS(body)(using directstyle(ks)).run(k)
229+
}
230+
231+
case cps.Stmt.LetCont(id, binding @ Cont.ContLam(result2, ks2, body2), body) =>
232+
Binding { k =>
233+
js.Const(nameDef(id), toJS(binding)(using nonrecursive(ks2))) :: requiringThunk { toJS(body) }.run(k)
231234
}
232235

233236
case cps.Stmt.Match(sc, Nil, None) =>
@@ -245,19 +248,30 @@ object TransformerCps extends Transformer {
245248
pure(js.Switch(js.Member(scrutinee, `tag`),
246249
clauses.map { case (tag, clause) =>
247250
val (e, binding) = toJS(scrutinee, tag, clause);
248-
(e, binding.stmts)
251+
252+
val stmts = binding.stmts
253+
254+
stmts.last match {
255+
case terminator : (js.Stmt.Return | js.Stmt.Break | js.Stmt.Continue) => (e, stmts)
256+
case other => (e, stmts :+ js.Break())
257+
}
249258
},
250259
default.map { s => toJS(s).stmts }))
251260

261+
case cps.Stmt.Jump(k, arg, ks) if D.directStyle.exists(c => c.k == k) => D.directStyle match {
262+
case Some(ContinuationInfo(k2, param2, ks2)) => pure(js.Assign(nameRef(param2), toJS(arg)))
263+
case None => sys error "Should not happen"
264+
}
265+
252266
case cps.Stmt.Jump(k, arg, ks) =>
253267
pure(js.Return(maybeThunking(js.Call(nameRef(k), toJS(arg), toJS(ks)))))
254268

255-
case cps.Stmt.App(callee @ DefInfo(id, vparams, bparams, ks1, k1, used), vargs, bargs, MetaCont(ks), Cont.ContVar(k)) if ks1 == ks && k1 == k =>
269+
case cps.Stmt.App(Recursive(id, vparams, bparams, ks1, k1, used), vargs, bargs, MetaCont(ks), Cont.ContVar(k)) if sameScope(ks, k, ks1, k1) =>
256270
Binding { k2 =>
257271
val stmts = mutable.ListBuffer.empty[js.Stmt]
258272
stmts.append(js.RawStmt("/* prepare tail call */"))
259273

260-
used.used = true
274+
used.jumped = true
261275

262276
// const x3 = [[ arg ]]; ...
263277
val vtmps = (vparams zip vargs).map { (id, arg) =>
@@ -336,13 +350,13 @@ object TransformerCps extends Transformer {
336350
}
337351

338352
case cps.Stmt.Reset(prog, ks, k) =>
339-
pure(js.Return(Call(RESET, toJS(prog)(using clearDefinitions), toJS(ks), toJS(k))))
353+
pure(js.Return(Call(RESET, toJS(prog)(using nonrecursive(prog)), toJS(ks), toJS(k))))
340354

341355
case cps.Stmt.Shift(prompt, body, ks, k) =>
342-
pure(js.Return(Call(SHIFT, nameRef(prompt), noThunking { toJS(body)(using clearDefinitions) }, toJS(ks), toJS(k))))
356+
pure(js.Return(Call(SHIFT, nameRef(prompt), noThunking { toJS(body)(using nonrecursive(body)) }, toJS(ks), toJS(k))))
343357

344358
case cps.Stmt.Resume(r, b, ks2, k2) =>
345-
pure(js.Return(js.Call(RESUME, nameRef(r), toJS(b)(using clearDefinitions), toJS(ks2), toJS(k2))))
359+
pure(js.Return(js.Call(RESUME, nameRef(r), toJS(b)(using nonrecursive(b)), toJS(ks2), toJS(k2))))
346360

347361
case cps.Stmt.Hole() =>
348362
pure(js.Return($effekt.call("hole")))
@@ -373,7 +387,7 @@ object TransformerCps extends Transformer {
373387
// Inlining Externs
374388
// ----------------
375389

376-
def inlineExtern(id: Id, args: List[cps.Pure])(using T: TransformerContext): js.Expr =
390+
private def inlineExtern(id: Id, args: List[cps.Pure])(using T: TransformerContext): js.Expr =
377391
T.externs.get(id) match {
378392
case Some(cps.Extern.Def(id, params, Nil, async,
379393
ExternBody.StringExternBody(featureFlag, Template(strings, templateArgs)))) if !async =>
@@ -383,11 +397,96 @@ object TransformerCps extends Transformer {
383397
case _ => js.Call(nameRef(id), args.map(toJS))
384398
}
385399

386-
def canInline(extern: cps.Extern): Boolean = extern match {
400+
private def canInline(extern: cps.Extern): Boolean = extern match {
387401
case cps.Extern.Def(_, _, Nil, async, ExternBody.StringExternBody(_, Template(_, _))) => !async
388402
case _ => false
389403
}
390404

405+
private def bindingAll[R](bs: List[(Id, js.Expr)])(body: TransformerContext ?=> R)(using C: TransformerContext): R =
406+
body(using C.copy(bindings = C.bindings ++ bs))
407+
408+
private def lookup(id: Id)(using C: TransformerContext): js.Expr = C.bindings.getOrElse(id, nameRef(id))
409+
410+
411+
// Helpers for Direct-Style Transformation
412+
// ---------------------------------------
413+
414+
/**
415+
* Used to determine whether a call with continuations [[ ks ]] (after substitution) and [[ k ]]
416+
* is the same as the original function definition (that is [[ ks1 ]] and [[ k1 ]].
417+
*/
418+
private def sameScope(ks: Id, k: Id, ks1: Id, k1: Id)(using C: TransformerContext): Boolean =
419+
ks1 == C.metaconts.getOrElse(ks, ks) && k1 == k
420+
421+
private def withDirectStyle(id: Id, param: Id, ks: Id)(using C: TransformerContext): TransformerContext =
422+
C.copy(directStyle = Some(ContinuationInfo(id, param, ks)))
423+
424+
private def recursive(id: Id, used: RecursiveUsage, block: cps.Block)(using C: TransformerContext): TransformerContext = block match {
425+
case cps.BlockLit(vparams, bparams, ks, k, body) =>
426+
C.copy(recursive = Some(RecursiveDefInfo(id, vparams, bparams, ks, k, used)), directStyle = None, metacont = Some(ks))
427+
case _ => C
428+
}
429+
430+
private def nonrecursive(ks: Id)(using C: TransformerContext): TransformerContext =
431+
C.copy(recursive = None, directStyle = None, metacont = Some(ks))
432+
433+
private def nonrecursive(block: cps.BlockLit)(using C: TransformerContext): TransformerContext = nonrecursive(block.ks)
434+
435+
// ks | let k1 x1 ks1 = { let k2 x2 ks2 = jump k v ks2 }; ... = jump k v ks
436+
private def directstyle(ks: Id)(using C: TransformerContext): TransformerContext =
437+
val outer = C.metacont.getOrElse { sys error "Metacontinuation missing..." }
438+
val outerSubstituted = C.metaconts.getOrElse(outer, outer)
439+
val subst = C.metaconts.updated(ks, outerSubstituted)
440+
C.copy(metacont = Some(ks), metaconts = subst)
441+
442+
private object Recursive {
443+
def unapply(b: cps.Block)(using C: TransformerContext): Option[(Id, List[Id], List[Id], Id, Id, RecursiveUsage)] = b match {
444+
case cps.Block.BlockVar(id) => C.recursive.collect {
445+
case RecursiveDefInfo(id2, vparams, bparams, ks, k, used) if id == id2 => (id, vparams, bparams, ks, k, used)
446+
}
447+
case _ => None
448+
}
449+
}
450+
451+
private def canBeDirect(k: Id, stmt: Stmt)(using T: TransformerContext): Boolean =
452+
def notIn(term: Stmt | Block | Expr | (Id, Clause) | Cont) =
453+
val freeVars = term match {
454+
case s: Stmt => free(s)
455+
case b: Block => free(b)
456+
case p: Expr => free(p)
457+
case (id, Clause(_, body)) => free(body)
458+
case c: Cont => free(c)
459+
}
460+
!freeVars.contains(k)
461+
stmt match {
462+
case Stmt.Jump(k2, arg, ks2) if k2 == k => notIn(arg) && T.metacont.contains(ks2.id)
463+
case Stmt.Jump(k2, arg, ks2) => notIn(arg)
464+
// TODO this could be a tailcall!
465+
case Stmt.App(callee, vargs, bargs, ks, k) => notIn(stmt)
466+
case Stmt.Invoke(callee, method, vargs, bargs, ks, k2) => notIn(stmt)
467+
case Stmt.If(cond, thn, els) => canBeDirect(k, thn) && canBeDirect(k, els)
468+
case Stmt.Match(scrutinee, clauses, default) => clauses.forall {
469+
case (id, Clause(vparams, body)) => canBeDirect(k, body)
470+
} && default.forall(body => canBeDirect(k, body))
471+
case Stmt.LetDef(id, binding, body) => notIn(binding) && canBeDirect(k, body)
472+
case Stmt.LetExpr(id, binding, body) => notIn(binding) && canBeDirect(k, body)
473+
case Stmt.LetCont(id, Cont.ContLam(result, ks2, body), body2) =>
474+
def willBeDirectItself = canBeDirect(id, body2) && canBeDirect(k, body)(using directstyle(ks2))
475+
def notFreeinContinuation = notIn(body) && canBeDirect(k, body2)
476+
willBeDirectItself || notFreeinContinuation
477+
case Stmt.Region(id, ks, body) => notIn(body)
478+
case Stmt.Alloc(id, init, region, body) => notIn(init) && canBeDirect(k, body)
479+
case Stmt.Var(id, init, ks2, body) => notIn(init) && canBeDirect(k, body)
480+
case Stmt.Dealloc(ref, body) => canBeDirect(k, body)
481+
case Stmt.Get(ref, id, body) => canBeDirect(k, body)
482+
case Stmt.Put(ref, value, body) => notIn(value) && canBeDirect(k, body)
483+
case Stmt.Reset(prog, ks, k) => notIn(stmt)
484+
case Stmt.Shift(prompt, body, ks, k) => notIn(stmt)
485+
case Stmt.Resume(resumption, body, ks, k) => notIn(stmt)
486+
case Stmt.Hole() => true
487+
}
488+
489+
391490

392491
// Thunking
393492
// --------

0 commit comments

Comments
 (0)