Skip to content

Commit fac98f5

Browse files
committed
Lambda impl methods static and more stably named
The body of lambdas is compiled into a synthetic method in the enclosing class. Previously, this method was a public virtual method named `fully$qualified$Class$$anonfun$n`. For lambdas that didn't capture a `this` reference, a static method was used. This commit changes two aspects. Firstly, all lambda impl methods are now emitted static. An extra parameter is added to those that require a this reference. This is an improvement as it: - allows, shorter, more readable names for the lambda impl method - avoids pollution of the vtable of the class. Note that javac uses private instance methods, rather than public static methods. If we followed its lead, we would be unable to support important use cases in our inliner Secondly, the name of the enclosing method has been included in the name of the lambda impl method to improve debuggability and to improve serialization compatibility. The serialization improvement comes from the way that fresh names for the impl methods are allocated: adding or removing lambdas in methods not named "foo" won't change the numbering of the `anonfun$foo$n` impl methods from methods named "foo". This is in line with user expectations about anonymous class and lambda serialization stability. Brian Goetz has described this tricky area well in: http://cr.openjdk.java.net/~briangoetz/eg-attachments/lambda-serialization.html This commit doesn't go as far a Javac, we don't use the hash of the lambda type info, param names, etc to map to a lambda impl method name. As such, we are more prone to the type-1 and -2 failures described there. However, our Scala 2.11.8 has similar characteristics, so we aren't going backwards. Special case in the naming: Use "new" rather than "<init>" for constructor enclosed lambdas, as javac does.
1 parent f66a230 commit fac98f5

17 files changed

+208
-103
lines changed

src/compiler/scala/tools/nsc/ast/TreeGen.scala

+47-3
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,25 @@ abstract class TreeGen extends scala.reflect.internal.TreeGen with TreeDSL {
269269
if (!sam.exists) mkMethodForFunctionBody(localTyper)(owner, fun, nme.apply)()
270270
else {
271271
val samMethType = fun.tpe memberInfo sam
272-
mkMethodForFunctionBody(localTyper)(owner, fun, sam.name.toTermName)(methParamProtos = samMethType.params, resTp = samMethType.resultType)
272+
val res = mkMethodForFunctionBody(localTyper)(owner, fun, sam.name.toTermName)(methParamProtos = samMethType.params, resTp = samMethType.resultType)
273+
res
273274
}
274275
}
275276

276277
// used to create the lifted method that holds a function's body
277-
def mkLiftedFunctionBodyMethod(localTyper: analyzer.Typer)(owner: Symbol, fun: Function) =
278-
mkMethodForFunctionBody(localTyper)(owner, fun, nme.ANON_FUN_NAME)(additionalFlags = ARTIFACT)
278+
def mkLiftedFunctionBodyMethod(localTyper: analyzer.Typer)(owner: Symbol, fun: Function, newNamePrefix: TermName) = {
279+
def nonLocalEnclosingMember(sym: Symbol) = {
280+
if (sym.isLocalDummy) sym.enclClass.primaryConstructor else {
281+
if (sym.isLocalToBlock) sym.originalOwner else sym
282+
}
283+
}
284+
val ownerName = nonLocalEnclosingMember(fun.symbol.originalOwner).name match {
285+
case nme.CONSTRUCTOR => nme.NEWkw // do as javac does for the suffix, prefer "new" to "$lessinit$greater$1"
286+
case x => x
287+
}
288+
val newName = newNamePrefix.append(nme.NAME_JOIN_STRING).append(ownerName)
289+
mkMethodForFunctionBody(localTyper)(owner, fun, newName)(additionalFlags = ARTIFACT)
290+
}
279291

280292

281293
/**
@@ -310,6 +322,38 @@ abstract class TreeGen extends scala.reflect.internal.TreeGen with TreeDSL {
310322
newDefDef(methSym, moveToMethod(useMethodParams(fun.body)))(tpt = TypeTree(resTp))
311323
}
312324

325+
/**
326+
* Create a new `DefDef` based on `orig` with an explicit self parameter.
327+
*
328+
* Details:
329+
* - Must by run after erasure
330+
* - If `maybeClone` is the identity function, this runs "in place"
331+
* and mutates the symbol of `orig`. `orig` should be discarded
332+
* - Symbol owners and returns are substituted, as are parameter symbols
333+
* - Recursive calls are not rewritten. This is correct if we assume
334+
* that we either:
335+
* - are in "in-place" mode, but can guarantee that no recursive calls exists
336+
* - are associating the RHS with a cloned symbol, but intend for the original
337+
* method to remain and for recursive calls to target it.
338+
*/
339+
final def mkStatic(orig: DefDef, maybeClone: Symbol => Symbol): DefDef = {
340+
assert(phase.erasedTypes, phase)
341+
assert(!orig.symbol.hasFlag(SYNCHRONIZED), orig.symbol.defString)
342+
val origSym = orig.symbol
343+
val origParams = orig.symbol.info.params
344+
val newSym = maybeClone(orig.symbol)
345+
newSym.setFlag(STATIC)
346+
// Add an explicit self parameter
347+
val selfParamSym = newSym.newSyntheticValueParam(newSym.owner.typeConstructor, nme.SELF)
348+
newSym.updateInfo(newSym.info match {
349+
case mt @ MethodType(params, res) => copyMethodType(mt, selfParamSym :: params, res)
350+
})
351+
val selfParam = ValDef(selfParamSym)
352+
val rhs = orig.rhs.substituteThis(newSym.owner, atPos(newSym.pos)(gen.mkAttributedIdent(selfParamSym)))
353+
.substituteSymbols(origParams, newSym.info.params.drop(1)).changeOwner(origSym -> newSym)
354+
treeCopy.DefDef(orig, orig.mods, orig.name, orig.tparams, (selfParam :: orig.vparamss.head) :: Nil, orig.tpt, rhs).setSymbol(newSym)
355+
}
356+
313357
// TODO: the rewrite to AbstractFunction is superfluous once we compile FunctionN to a SAM type (aka functional interface)
314358
def functionClassType(fun: Function): Type =
315359
if (isFunctionType(fun.tpe)) abstractFunctionType(fun.vparams.map(_.symbol.tpe), fun.body.tpe.deconst)

src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class BackendUtils[BT <: BTypes](val btypes: BT) {
121121

122122
def getBoxedUnit: FieldInsnNode = new FieldInsnNode(GETSTATIC, srBoxedUnitRef.internalName, "UNIT", srBoxedUnitRef.descriptor)
123123

124-
private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""".r
124+
private val anonfunAdaptedName = """.*\$anonfun\$.*\$\d+\$adapted""".r
125125
def hasAdaptedImplMethod(closureInit: ClosureInstantiation): Boolean = {
126126
isBuiltinFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) &&
127127
anonfunAdaptedName.pattern.matcher(closureInit.lambdaMetaFactoryCall.implMethod.getName).matches

src/compiler/scala/tools/nsc/transform/Delambdafy.scala

+21-8
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,18 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
6161

6262
private def mkLambdaMetaFactoryCall(fun: Function, target: Symbol, functionalInterface: Symbol, samUserDefined: Symbol, isSpecialized: Boolean): Tree = {
6363
val pos = fun.pos
64+
def isSelfParam(p: Symbol) = p.isSynthetic && p.name == nme.SELF
65+
val hasSelfParam = isSelfParam(target.firstParam)
66+
6467
val allCapturedArgRefs = {
6568
// find which variables are free in the lambda because those are captures that need to be
6669
// passed into the constructor of the anonymous function class
6770
val captureArgs = FreeVarTraverser.freeVarsOf(fun).iterator.map(capture =>
6871
gen.mkAttributedRef(capture) setPos pos
6972
).toList
7073

71-
if (target hasFlag STATIC) captureArgs // no `this` reference needed
74+
if (!hasSelfParam) captureArgs.filterNot(arg => isSelfParam(arg.symbol))
75+
else if (currentMethod.hasFlag(Flags.STATIC)) captureArgs
7276
else (gen.mkAttributedThis(fun.symbol.enclClass) setPos pos) :: captureArgs
7377
}
7478

@@ -179,7 +183,7 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
179183
val numCaptures = targetParams.length - functionParamTypes.length
180184
val (targetCapturedParams, targetFunctionParams) = targetParams.splitAt(numCaptures)
181185

182-
val methSym = oldClass.newMethod(target.name.append("$adapted").toTermName, target.pos, target.flags | FINAL | ARTIFACT)
186+
val methSym = oldClass.newMethod(target.name.append("$adapted").toTermName, target.pos, target.flags | FINAL | ARTIFACT | STATIC)
183187
val bridgeCapturedParams = targetCapturedParams.map(param => methSym.newSyntheticValueParam(param.tpe, param.name.toTermName))
184188
val bridgeFunctionParams =
185189
map2(targetFunctionParams, bridgeParamTypes)((param, tp) => methSym.newSyntheticValueParam(tp, param.name.toTermName))
@@ -223,10 +227,8 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
223227

224228
private def transformFunction(originalFunction: Function): Tree = {
225229
val target = targetMethod(originalFunction)
226-
target.makeNotPrivate(target.owner)
227-
228-
// must be done before calling createBoxingBridgeMethod and mkLambdaMetaFactoryCall
229-
if (!(target hasFlag STATIC) && !methodReferencesThis(target)) target setFlag STATIC
230+
assert(target.hasFlag(Flags.STATIC))
231+
target.setFlag(notPRIVATE)
230232

231233
val funSym = originalFunction.tpe.typeSymbolDirect
232234
// The functional interface that can be used to adapt the lambda target method `target` to the given function type.
@@ -252,11 +254,22 @@ abstract class Delambdafy extends Transform with TypingTransformers with ast.Tre
252254
// here's the main entry point of the transform
253255
override def transform(tree: Tree): Tree = tree match {
254256
// the main thing we care about is lambdas
255-
case fun: Function => super.transform(transformFunction(fun))
257+
case fun: Function =>
258+
super.transform(transformFunction(fun))
256259
case Template(_, _, _) =>
260+
def pretransform(tree: Tree): Tree = tree match {
261+
case dd: DefDef if dd.symbol.isDelambdafyTarget =>
262+
if (!dd.symbol.hasFlag(STATIC) && methodReferencesThis(dd.symbol)) {
263+
gen.mkStatic(dd, sym => sym)
264+
} else {
265+
dd.symbol.setFlag(STATIC)
266+
dd
267+
}
268+
case t => t
269+
}
257270
try {
258271
// during this call boxingBridgeMethods will be populated from the Function case
259-
val Template(parents, self, body) = super.transform(tree)
272+
val Template(parents, self, body) = super.transform(deriveTemplate(tree)(_.mapConserve(pretransform)))
260273
Template(parents, self, body ++ boxingBridgeMethods)
261274
} finally boxingBridgeMethods.clear()
262275
case _ => super.transform(tree)

src/compiler/scala/tools/nsc/transform/UnCurry.scala

+13-4
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,23 @@ abstract class UnCurry extends InfoTransform
213213
// Expand the function body into an anonymous class
214214
gen.expandFunction(localTyper)(fun, inConstructorFlag)
215215
} else {
216+
val mustExpand = mustExpandFunction(fun)
217+
val newNamePrefix = if (mustExpand) nme.DELAMBDAFY_LAMBDA_CLASS_NAME else nme.ANON_FUN_NAME
216218
// method definition with the same arguments, return type, and body as the original lambda
217-
val liftedMethod = gen.mkLiftedFunctionBodyMethod(localTyper)(fun.symbol.owner, fun)
219+
val liftedMethod = gen.mkLiftedFunctionBodyMethod(localTyper)(fun.symbol.owner, fun, newNamePrefix)
218220

219221
// new function whose body is just a call to the lifted method
220222
val newFun = deriveFunction(fun)(_ => localTyper.typedPos(fun.pos)(
221223
gen.mkForwarder(gen.mkAttributedRef(liftedMethod.symbol), (fun.vparams map (_.symbol)) :: Nil)
222224
))
223225

224226
val typedNewFun = localTyper.typedPos(fun.pos)(Block(liftedMethod, super.transform(newFun)))
225-
if (mustExpandFunction(fun)) {
227+
if (mustExpand) {
226228
val Block(stats, expr : Function) = typedNewFun
227229
treeCopy.Block(typedNewFun, stats, gen.expandFunction(localTyper)(expr, inConstructorFlag))
228-
} else typedNewFun
230+
} else {
231+
typedNewFun
232+
}
229233
}
230234

231235
def transformArgs(pos: Position, fun: Symbol, args: List[Tree], formals: List[Type]) = {
@@ -341,13 +345,18 @@ abstract class UnCurry extends InfoTransform
341345

342346
private def isSelfSynchronized(ddef: DefDef) = ddef.rhs match {
343347
case Apply(fn @ TypeApply(Select(sel, _), _), _) =>
344-
fn.symbol == Object_synchronized && sel.symbol == ddef.symbol.enclClass && !ddef.symbol.enclClass.isTrait
348+
fn.symbol == Object_synchronized && sel.symbol == ddef.symbol.enclClass && !ddef.symbol.enclClass.isTrait &&
349+
!ddef.symbol.isDelambdafyTarget /* these become static later, unsuitable for ACC_SYNCHRONIZED */
345350
case _ => false
346351
}
347352

348353
/** If an eligible method is entirely wrapped in a call to synchronized
349354
* locked on the same instance, remove the synchronized scaffolding and
350355
* mark the method symbol SYNCHRONIZED for bytecode generation.
356+
*
357+
* Delambdafy targets are deemed ineligible as the Delambdafy phase will
358+
* replace `this.synchronized` with `$this.synchronzed` now that it emits
359+
* all lambda impl methods as static.
351360
*/
352361
private def translateSynchronized(tree: Tree) = tree match {
353362
case dd @ DefDef(_, _, _, _, _, Apply(fn, body :: Nil)) if isSelfSynchronized(dd) =>

src/partest-extras/scala/tools/partest/ASMConverters.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package scala.tools.partest
22

33
import scala.collection.JavaConverters._
44
import scala.tools.asm
5-
import asm.{tree => t}
5+
import asm.{Opcodes, tree => t}
66

77
/** Makes using ASM from ByteCodeTests more convenient.
88
*

test/files/run/delambdafy_t6028.check

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ package <empty> {
1111
def foo(methodParam: String): Function0 = {
1212
val methodLocal: String = "";
1313
{
14-
(() => T.this.$anonfun$1(methodParam, methodLocal))
14+
(() => T.this.$anonfun$foo$1(methodParam, methodLocal))
1515
}
1616
};
1717
def bar(barParam: String): Object = {
@@ -21,10 +21,10 @@ package <empty> {
2121
def tryy(tryyParam: String): Function0 = {
2222
var tryyLocal: runtime.ObjectRef = scala.runtime.ObjectRef.create("");
2323
{
24-
(() => T.this.$anonfun$2(tryyParam, tryyLocal))
24+
(() => T.this.$anonfun$tryy$1(tryyParam, tryyLocal))
2525
}
2626
};
27-
final <artifact> private[this] def $anonfun$1(methodParam$1: String, methodLocal$1: String): String = T.this.classParam.+(T.this.field()).+(methodParam$1).+(methodLocal$1);
27+
final <artifact> private[this] def $anonfun$foo$1(methodParam$1: String, methodLocal$1: String): String = T.this.classParam.+(T.this.field()).+(methodParam$1).+(methodLocal$1);
2828
abstract trait MethodLocalTrait$1 extends Object {
2929
def /*MethodLocalTrait$1*/$init$(barParam$1: String): Unit = {
3030
()
@@ -54,7 +54,7 @@ package <empty> {
5454
T.this.MethodLocalObject$lzycompute$1(barParam$1, MethodLocalObject$module$1)
5555
else
5656
MethodLocalObject$module$1.elem.$asInstanceOf[T#MethodLocalObject$2.type]();
57-
final <artifact> private[this] def $anonfun$2(tryyParam$1: String, tryyLocal$1: runtime.ObjectRef): Unit = try {
57+
final <artifact> private[this] def $anonfun$tryy$1(tryyParam$1: String, tryyLocal$1: runtime.ObjectRef): Unit = try {
5858
tryyLocal$1.elem = tryyParam$1
5959
} finally ()
6060
}

test/files/run/delambdafy_t6555.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ package <empty> {
66
()
77
};
88
private[this] val f: String => String = {
9-
final <artifact> def $anonfun(param: String): String = param;
10-
((param: String) => $anonfun(param))
9+
final <artifact> def $anonfun$f(param: String): String = param;
10+
((param: String) => $anonfun$f(param))
1111
};
1212
<stable> <accessor> def f(): String => String = Foo.this.f
1313
}

test/files/run/delambdafy_uncurry_byname_method.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ package <empty> {
77
};
88
def bar(x: () => String): String = x.apply();
99
def foo(): String = Foo.this.bar({
10-
final <artifact> def $anonfun(): String = "";
11-
(() => $anonfun())
10+
final <artifact> def $anonfun$foo(): String = "";
11+
(() => $anonfun$foo())
1212
})
1313
}
1414
}

test/files/run/delambdafy_uncurry_method.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ package <empty> {
77
};
88
def bar(): Unit = {
99
val f: Int => Int = {
10-
final <artifact> def $anonfun(x: Int): Int = x.+(1);
11-
((x: Int) => $anonfun(x))
10+
final <artifact> def $anonfun|(x: Int): Int = x.+(1);
11+
((x: Int) => $anonfun|(x))
1212
};
1313
()
1414
}

test/files/run/t9097.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ object Test extends StoreReporterDirectTest {
2828
assert(!storeReporter.hasErrors, message = filteredInfos map (_.msg) mkString "; ")
2929
val out = baos.toString("UTF-8")
3030
// was 2 before the fix, the two PackageDefs for a would both contain the ClassDef for the closure
31-
assert(out.lines.count(_ contains "def $anonfun$1(x$1: Int): String") == 1, out)
31+
assert(out.lines.count(_ contains "def $anonfun$hihi$1(x$1: Int): String") == 1, out)
3232
}
3333
}

test/junit/scala/issues/OptimizedBytecodeTest.scala

+9-10
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ package scala.issues
22

33
import org.junit.runner.RunWith
44
import org.junit.runners.JUnit4
5-
import org.junit.Test
5+
import org.junit.{AfterClass, Test}
6+
67
import scala.tools.asm.Opcodes._
78
import org.junit.Assert._
89

910
import scala.tools.nsc.backend.jvm.{AsmUtils, CodeGenTools}
10-
1111
import CodeGenTools._
1212
import scala.tools.partest.ASMConverters
1313
import ASMConverters._
1414
import AsmUtils._
15-
1615
import scala.tools.testing.ClearAfterClass
1716

1817
object OptimizedBytecodeTest extends ClearAfterClass.Clearable {
@@ -56,9 +55,9 @@ class OptimizedBytecodeTest extends ClearAfterClass {
5655
val List(c) = compileClasses(compiler)(code)
5756

5857
assertSameSummary(getSingleMethod(c, "t"), List(
59-
LDC, ASTORE, ALOAD /*0*/, ALOAD /*1*/, "C$$$anonfun$1", IRETURN))
60-
assertSameSummary(getSingleMethod(c, "C$$$anonfun$1"), List(LDC, "C$$$anonfun$2", IRETURN))
61-
assertSameSummary(getSingleMethod(c, "C$$$anonfun$2"), List(-1 /*A*/, GOTO /*A*/))
58+
LDC, ASTORE, ALOAD /*0*/, ALOAD /*1*/, "$anonfun$t$1", IRETURN))
59+
assertSameSummary(getSingleMethod(c, "$anonfun$t$1"), List(ALOAD, IFNONNULL /*8*/, ACONST_NULL, ATHROW, -1 /*8*/, LDC, "$anonfun$t$2", IRETURN))
60+
assertSameSummary(getSingleMethod(c, "$anonfun$t$2"), List(-1 /*A*/, GOTO /*A*/))
6261
}
6362

6463
@Test
@@ -308,9 +307,9 @@ class OptimizedBytecodeTest extends ClearAfterClass {
308307
|}
309308
""".stripMargin
310309
val List(c) = compileClasses(compiler)(code, allowMessage = _.msg.contains("exception handler declared in the inlined method"))
311-
assertInvoke(getSingleMethod(c, "f1a"), "C", "C$$$anonfun$1")
310+
assertInvoke(getSingleMethod(c, "f1a"), "C", "$anonfun$f1a$1")
312311
assertInvoke(getSingleMethod(c, "f1b"), "C", "wrapper1")
313-
assertInvoke(getSingleMethod(c, "f2a"), "C", "C$$$anonfun$3")
312+
assertInvoke(getSingleMethod(c, "f2a"), "C", "$anonfun$f2a$1")
314313
assertInvoke(getSingleMethod(c, "f2b"), "C", "wrapper2")
315314
}
316315

@@ -344,7 +343,7 @@ class OptimizedBytecodeTest extends ClearAfterClass {
344343
|class Listt
345344
""".stripMargin
346345
val List(c, nil, nilMod, listt) = compileClasses(compiler)(code)
347-
assertInvoke(getSingleMethod(c, "t"), "C", "C$$$anonfun$1")
346+
assertInvoke(getSingleMethod(c, "t"), "C", "$anonfun$t$1")
348347
}
349348

350349
@Test
@@ -370,6 +369,6 @@ class OptimizedBytecodeTest extends ClearAfterClass {
370369
def optimiseEnablesNewOpt(): Unit = {
371370
val code = """class C { def t = (1 to 10) foreach println }"""
372371
val List(c) = readAsmClasses(compile(newCompiler(extraArgs = "-optimise -deprecation"))(code, allowMessage = _.msg.contains("is deprecated")))
373-
assertInvoke(getSingleMethod(c, "t"), "C", "C$$$anonfun$1") // range-foreach inlined from classpath
372+
assertInvoke(getSingleMethod(c, "t"), "C", "$anonfun$t$1") // range-foreach inlined from classpath
374373
}
375374
}

test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ object CodeGenTools {
213213
}
214214

215215
def getSingleMethod(classNode: ClassNode, name: String): Method =
216-
convertMethod(classNode.methods.asScala.toList.find(_.name == name).get)
216+
convertMethod(classNode.methods.asScala.toList.find(_.name == name).getOrElse(
217+
throw new NoSuchElementException(s"$name not found in: ${classNode.methods.asScala.toList.map(_.name)}"))
218+
)
217219

218220
def findAsmMethods(c: ClassNode, p: String => Boolean) = c.methods.iterator.asScala.filter(m => p(m.name)).toList.sortBy(_.name)
219221
def findAsmMethod(c: ClassNode, name: String) = findAsmMethods(c, _ == name).head
@@ -229,6 +231,14 @@ object CodeGenTools {
229231
if (useNext) insns.map(_.getNext) else insns
230232
}
231233

234+
def getInstr(method: MethodNode, query: String): AbstractInsnNode = {
235+
findInstr(method, query) match {
236+
case i :: Nil => i
237+
case Nil => throw new AssertionError(s"No instruction matches '$query'. ${method.instructions.iterator.asScala.map(textify).toList}")
238+
case xs => throw new AssertionError(s"Too many instructions match '$query'. ${xs.map(textify)}")
239+
}
240+
}
241+
232242
def assertHandlerLabelPostions(h: ExceptionHandler, instructions: List[Instruction], startIndex: Int, endIndex: Int, handlerIndex: Int): Unit = {
233243
val insVec = instructions.toVector
234244
assertTrue(h.start == insVec(startIndex) && h.end == insVec(endIndex) && h.handler == insVec(handlerIndex))

0 commit comments

Comments
 (0)