Skip to content

Commit 6dda6c2

Browse files
committed
Introduce best-effort compilation for IDEs
2 new experimental options are introduces for the compiler: `-Ybest-effort` and `-Ywith-best-effort-tasty`. A related Best Effort TASTy (.betasty) format, a TASTy aligned file format able to hold some errored trees was also added. Behaviour of the options and the format is documented as part of this commit in the `best-effort-compilation.md` docs file. `-Ybest-effort` is used to produce `.betasty` files in the `<output>/META-INF/best-effort`. `-Ywith-best-effort-tasty` allows to use them during compilation, limiting it to the frontend phases if such file is used. If any .betasty is used, transparent inline macros also cease to be expanded by the compiler. Since best-effort compilation can fail (e.g. due to cyclic reference errors which sometimes are not able to be pickled or unpickled), the crashes caused by it are wrapped into an additional descriptive error message in the aim to fail more gracefully (and not pollute our issue tracker with known problems). The feature is tested in two ways: * with a set of pairs of dependent projects, one of which is meant to produce .betasty by using `-Ybest-effort`, and the other tries to consume it using `-Ywith-best-effort-tasty`. * by reusing the compiler nonbootstrapped neg tests, first by running them with `-Ybest-effort` option, and then by running read-tasty tests on the produced betasty files to thest best-effort tastt unpickling Additionally, `-Ywith-best-effort-tasty` allows to print `.betasty` via `-print-tasty`.
1 parent 45f633d commit 6dda6c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1022
-197
lines changed

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class GenBCode extends Phase { self =>
1616

1717
override def description: String = GenBCode.description
1818

19+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
20+
1921
private val superCallsMap = new MutableSymbolMap[Set[ClassSymbol]]
2022
def registerSuperCall(sym: Symbol, calls: ClassSymbol): Unit = {
2123
val old = superCallsMap.getOrElse(sym, Set.empty)

compiler/src/dotty/tools/backend/sjs/GenSJSIR.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class GenSJSIR extends Phase {
1212
override def description: String = GenSJSIR.description
1313

1414
override def isRunnable(using Context): Boolean =
15-
super.isRunnable && ctx.settings.scalajs.value
15+
super.isRunnable && ctx.settings.scalajs.value && !ctx.usesBestEffortTasty
1616

1717
def run(using Context): Unit =
1818
new JSCodeGen().run()

compiler/src/dotty/tools/dotc/Driver.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class Driver {
3939
catch
4040
case ex: FatalError =>
4141
report.error(ex.getMessage.nn) // signals that we should fail compilation.
42+
case ex: Throwable if ctx.usesBestEffortTasty =>
43+
report.bestEffortError(ex, "Some best-effort tasty files were not able to be read.")
4244
case ex: TypeError if !runOrNull.enrichedErrorMessage =>
4345
println(runOrNull.enrichErrorMessage(s"${ex.toMessage} while compiling ${files.map(_.path).mkString(", ")}"))
4446
throw ex
@@ -100,8 +102,8 @@ class Driver {
100102
None
101103
else file.extension match
102104
case "jar" => Some(file.path)
103-
case "tasty" =>
104-
TastyFileUtil.getClassPath(file) match
105+
case "tasty" | "betasty" =>
106+
TastyFileUtil.getClassPath(file, ctx.withBestEffortTasty) match
105107
case Some(classpath) => Some(classpath)
106108
case _ =>
107109
report.error(em"Could not load classname from: ${file.path}")

compiler/src/dotty/tools/dotc/Run.scala

+12-1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
230230
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
231231
ctx.base.usePhases(phases)
232232

233+
var forceReachPhaseMaybe =
234+
if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
235+
else None
236+
233237
if ctx.settings.YnoDoubleBindings.value then
234238
ctx.base.checkNoDoubleBindings = true
235239

@@ -239,7 +243,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
239243
var phasesWereAdjusted = false
240244

241245
for (phase <- ctx.base.allPhases)
242-
if (phase.isRunnable)
246+
if (phase.isRunnable || forceReachPhaseMaybe.nonEmpty)
243247
Stats.trackTime(s"phase time ms/$phase") {
244248
val start = System.currentTimeMillis
245249
val profileBefore = profiler.beforePhase(phase)
@@ -249,6 +253,13 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
249253
for (unit <- units)
250254
lastPrintedTree =
251255
printTree(lastPrintedTree)(using ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
256+
257+
forceReachPhaseMaybe match {
258+
case Some(forceReachPhase) if phase.phaseName == forceReachPhase =>
259+
forceReachPhaseMaybe = None
260+
case _ =>
261+
}
262+
252263
report.informTime(s"$phase ", start)
253264
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
254265
for (unit <- units)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -893,12 +893,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
893893
else cpy.PackageDef(tree)(pid, slicedStats) :: Nil
894894
case tdef: TypeDef =>
895895
val sym = tdef.symbol
896-
assert(sym.isClass)
896+
if !ctx.isBestEffort then assert(sym.isClass)
897897
if (cls == sym || cls == sym.linkedClass) tdef :: Nil
898898
else Nil
899899
case vdef: ValDef =>
900900
val sym = vdef.symbol
901-
assert(sym.is(Module))
901+
if !ctx.isBestEffort then assert(sym.is(Module))
902902
if (cls == sym.companionClass || cls == sym.moduleClass) vdef :: Nil
903903
else Nil
904904
case tree =>

compiler/src/dotty/tools/dotc/ast/tpd.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
4848
case _: RefTree | _: GenericApply | _: Inlined | _: Hole =>
4949
ta.assignType(untpd.Apply(fn, args), fn, args)
5050
case _ =>
51-
assert(ctx.reporter.errorsReported)
51+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
5252
ta.assignType(untpd.Apply(fn, args), fn, args)
5353

5454
def TypeApply(fn: Tree, args: List[Tree])(using Context): TypeApply = fn match
@@ -57,7 +57,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
5757
case _: RefTree | _: GenericApply =>
5858
ta.assignType(untpd.TypeApply(fn, args), fn, args)
5959
case _ =>
60-
assert(ctx.reporter.errorsReported)
60+
assert(ctx.isBestEffort || ctx.usesBestEffortTasty || ctx.reporter.errorsReported)
6161
ta.assignType(untpd.TypeApply(fn, args), fn, args)
6262

6363
def Literal(const: Constant)(using Context): Literal =

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
288288

289289
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
290290
protected def isMatchingFile(f: JFile): Boolean =
291-
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
291+
f.isTasty || f.isBestEffortTasty || (f.isClass && f.classToTasty.isEmpty)
292292

293293
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
294294
}

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

+7
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ object FileUtils {
2424

2525
def hasTastyExtension: Boolean = file.hasExtension("tasty")
2626

27+
def hasBetastyExtension: Boolean = file.hasExtension("betasty")
28+
2729
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2830

31+
def isBestEffortTasty: Boolean = !file.isDirectory && hasBetastyExtension
32+
2933
def isScalaBinary: Boolean = file.isClass || file.isTasty
3034

3135
def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java"))
@@ -54,6 +58,8 @@ object FileUtils {
5458

5559
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
5660

61+
def isBestEffortTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_BETASTY)
62+
5763
/** Returns the tasty file associated with this class file */
5864
def classToTasty: Option[JFile] =
5965
assert(file.isClass, s"non-class: $file")
@@ -66,6 +72,7 @@ object FileUtils {
6672
private val SUFFIX_CLASS = ".class"
6773
private val SUFFIX_SCALA = ".scala"
6874
private val SUFFIX_TASTY = ".tasty"
75+
private val SUFFIX_BETASTY = ".betasty"
6976
private val SUFFIX_JAVA = ".java"
7077
private val SUFFIX_SIG = ".sig"
7178

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+3
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ private sealed trait YSettings:
380380
val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_")
381381
//.withPostSetHook( _ => YprofileEnabled.value = true )
382382

383+
val YbestEffort: Setting[Boolean] = BooleanSetting("-Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.")
384+
val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting("-Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.")
385+
383386
// Experimental language features
384387
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.")
385388
val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.")

compiler/src/dotty/tools/dotc/core/Contexts.scala

+15
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,18 @@ object Contexts {
463463
/** Is the explicit nulls option set? */
464464
def explicitNulls: Boolean = base.settings.YexplicitNulls.value
465465

466+
/** Is best-effort-dir option set? */
467+
def isBestEffort: Boolean = base.settings.YbestEffort.value
468+
469+
/** Is the from-best-effort-tasty option set to true? */
470+
def withBestEffortTasty: Boolean = base.settings.YwithBestEffortTasty.value
471+
472+
/** Were any best effort tasty dependencies used during compilation? */
473+
def usesBestEffortTasty: Boolean = base.usedBestEffortTasty
474+
475+
/** Confirm that a best effort tasty dependency was used during compilation. */
476+
def setUsesBestEffortTasty(): Unit = base.usedBestEffortTasty = true
477+
466478
/** A fresh clone of this context embedded in this context. */
467479
def fresh: FreshContext = freshOver(this)
468480

@@ -949,6 +961,9 @@ object Contexts {
949961
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
950962
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
951963

964+
/** Was best effort file used during compilation? */
965+
private[core] var usedBestEffortTasty = false
966+
952967
// Types state
953968
/** A table for hash consing unique types */
954969
private[core] val uniques: Uniques = Uniques()

compiler/src/dotty/tools/dotc/core/DenotTransformers.scala

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object DenotTransformers {
2828

2929
/** The transformation method */
3030
def transform(ref: SingleDenotation)(using Context): SingleDenotation
31+
32+
override def isRunnable(using Context) = super.isRunnable && !ctx.usesBestEffortTasty
3133
}
3234

3335
/** A transformer that only transforms the info field of denotations */

compiler/src/dotty/tools/dotc/core/Denotations.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,8 @@ object Denotations {
717717
ctx.runId >= validFor.runId
718718
|| ctx.settings.YtestPickler.value // mixing test pickler with debug printing can travel back in time
719719
|| ctx.mode.is(Mode.Printing) // no use to be picky when printing error messages
720-
|| symbol.isOneOf(ValidForeverFlags),
720+
|| symbol.isOneOf(ValidForeverFlags)
721+
|| ctx.isBestEffort || ctx.usesBestEffortTasty,
721722
s"denotation $this invalid in run ${ctx.runId}. ValidFor: $validFor")
722723
var d: SingleDenotation = this
723724
while ({

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

+11-7
Original file line numberDiff line numberDiff line change
@@ -711,12 +711,16 @@ object SymDenotations {
711711
* TODO: Find a more robust way to characterize self symbols, maybe by
712712
* spending a Flag on them?
713713
*/
714-
final def isSelfSym(using Context): Boolean = owner.infoOrCompleter match {
715-
case ClassInfo(_, _, _, _, selfInfo) =>
716-
selfInfo == symbol ||
717-
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
718-
case _ => false
719-
}
714+
final def isSelfSym(using Context): Boolean =
715+
if !ctx.isBestEffort || exists then
716+
owner.infoOrCompleter match {
717+
case ClassInfo(_, _, _, _, selfInfo) =>
718+
selfInfo == symbol ||
719+
selfInfo.isInstanceOf[Type] && name == nme.WILDCARD
720+
case _ => false
721+
}
722+
else false
723+
720724

721725
/** Is this definition contained in `boundary`?
722726
* Same as `ownersIterator contains boundary` but more efficient.
@@ -1990,7 +1994,7 @@ object SymDenotations {
19901994
case p :: parents1 =>
19911995
p.classSymbol match {
19921996
case pcls: ClassSymbol => builder.addAll(pcls.baseClasses)
1993-
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive), s"$this has non-class parent: $p")
1997+
case _ => assert(isRefinementClass || p.isError || ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty, s"$this has non-class parent: $p")
19941998
}
19951999
traverse(parents1)
19962000
case nil =>

compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala

+26-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import java.nio.channels.ClosedByInterruptException
77

88
import scala.util.control.NonFatal
99

10-
import dotty.tools.dotc.classpath.FileUtils.isTasty
10+
import dotty.tools.dotc.classpath.FileUtils.{isTasty, isBestEffortTasty}
1111
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
1212
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
1313

@@ -25,6 +25,7 @@ import ast.desugar
2525
import parsing.JavaParsers.OutlineJavaParser
2626
import parsing.Parsers.OutlineParser
2727
import dotty.tools.tasty.TastyHeaderUnpickler
28+
import dotty.tools.tasty.besteffort.BestEffortTastyHeaderUnpickler
2829

2930

3031
object SymbolLoaders {
@@ -198,7 +199,7 @@ object SymbolLoaders {
198199
enterToplevelsFromSource(owner, nameOf(classRep), src)
199200
case (Some(bin), _) =>
200201
val completer =
201-
if bin.isTasty then ctx.platform.newTastyLoader(bin)
202+
if bin.isTasty || bin.isBestEffortTasty then ctx.platform.newTastyLoader(bin)
202203
else ctx.platform.newClassLoader(bin)
203204
enterClassAndModule(owner, nameOf(classRep), completer)
204205
}
@@ -261,7 +262,8 @@ object SymbolLoaders {
261262
(idx + str.TOPLEVEL_SUFFIX.length + 1 != name.length || !name.endsWith(str.TOPLEVEL_SUFFIX))
262263
}
263264

264-
def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$'
265+
def maybeModuleClass(classRep: ClassRepresentation): Boolean =
266+
classRep.name.nonEmpty && classRep.name.last == '$'
265267

266268
private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = {
267269
def isAbsent(classRep: ClassRepresentation) =
@@ -418,17 +420,27 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
418420

419421
override def sourceFileOrNull: AbstractFile | Null = tastyFile
420422

421-
def description(using Context): String = "TASTy file " + tastyFile.toString
423+
def description(using Context): String =
424+
if tastyFile.extension == ".betasty" then "Best Effort TASTy file " + tastyFile.toString
425+
else "TASTy file " + tastyFile.toString
422426

423427
override def doComplete(root: SymDenotation)(using Context): Unit =
424428
val (classRoot, moduleRoot) = rootDenots(root.asClass)
425-
val tastyBytes = tastyFile.toByteArray
426-
val unpickler = new tasty.DottyUnpickler(tastyBytes)
427-
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
428-
if mayLoadTreesFromTasty then
429-
classRoot.classSymbol.rootTreeOrProvider = unpickler
430-
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
431-
checkTastyUUID(tastyFile, tastyBytes)
429+
val isBestEffortTasty = tastyFile.name.endsWith(".betasty")
430+
if (!isBestEffortTasty || ctx.withBestEffortTasty) then
431+
val tastyBytes = tastyFile.toByteArray
432+
val unpickler = new tasty.DottyUnpickler(tastyBytes, isBestEffortTasty = isBestEffortTasty)
433+
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
434+
if mayLoadTreesFromTasty || (isBestEffortTasty && ctx.withBestEffortTasty) then
435+
classRoot.classSymbol.rootTreeOrProvider = unpickler
436+
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
437+
if isBestEffortTasty then
438+
checkBeTastyUUID(tastyFile, tastyBytes)
439+
ctx.setUsesBestEffortTasty()
440+
else
441+
checkTastyUUID(tastyFile, tastyBytes)
442+
else
443+
report.error(em"$tastyFile Best Effort TASTy file could not be read.")
432444

433445

434446
private def checkTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
@@ -442,6 +454,9 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
442454
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`
443455
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
444456

457+
private def checkBeTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
458+
new BestEffortTastyHeaderUnpickler(tastyBytes).readHeader()
459+
445460
private def mayLoadTreesFromTasty(using Context): Boolean =
446461
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value
447462
}

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,8 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
759759
MissingType(tycon.prefix, tycon.name).toMessage.message
760760
case _ =>
761761
i"Cannot resolve reference to $tp"
762-
throw FatalError(msg)
762+
if ctx.isBestEffort then report.error(msg)
763+
else throw FatalError(msg)
763764
tp1
764765

765766
/** Widen term ref, skipping any `()` parameter of an eventual getter. Used to erase a TermRef.

compiler/src/dotty/tools/dotc/core/Types.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -3051,7 +3051,8 @@ object Types {
30513051
if (ctx.erasedTypes) tref
30523052
else cls.info match {
30533053
case cinfo: ClassInfo => cinfo.selfType
3054-
case _: ErrorType | NoType if ctx.mode.is(Mode.Interactive) => cls.info
3054+
case _: ErrorType | NoType
3055+
if ctx.mode.is(Mode.Interactive) || ctx.isBestEffort || ctx.usesBestEffortTasty => cls.info
30553056
// can happen in IDE if `cls` is stale
30563057
}
30573058

@@ -3574,8 +3575,8 @@ object Types {
35743575

35753576
def apply(tp1: Type, tp2: Type, soft: Boolean)(using Context): OrType = {
35763577
def where = i"in union $tp1 | $tp2"
3577-
expectValueTypeOrWildcard(tp1, where)
3578-
expectValueTypeOrWildcard(tp2, where)
3578+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp1, where)
3579+
if (!ctx.usesBestEffortTasty) expectValueTypeOrWildcard(tp2, where)
35793580
assertUnerased()
35803581
unique(new CachedOrType(tp1, tp2, soft))
35813582
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dotty.tools.dotc
2+
package core
3+
package tasty
4+
5+
import scala.language.unsafeNulls
6+
import java.nio.file.{Path as JPath, Files as JFiles}
7+
import java.nio.channels.ClosedByInterruptException
8+
import java.io.DataOutputStream
9+
import dotty.tools.io.{File, PlainFile}
10+
import dotty.tools.dotc.core.Contexts.Context
11+
12+
object BestEffortTastyWriter:
13+
14+
def write(dir: JPath, units: List[CompilationUnit])(using Context): Unit =
15+
if JFiles.exists(dir) then JFiles.createDirectories(dir)
16+
17+
units.foreach { unit =>
18+
unit.pickled.foreach { (clz, binary) =>
19+
val parts = clz.fullName.mangledString.split('.')
20+
val outPath = outputPath(parts.toList, dir)
21+
val outTastyFile = new PlainFile(new File(outPath))
22+
val outstream = new DataOutputStream(outTastyFile.bufferedOutput)
23+
try outstream.write(binary())
24+
catch case ex: ClosedByInterruptException =>
25+
try
26+
outTastyFile.delete() // don't leave an empty or half-written tastyfile around after an interrupt
27+
catch
28+
case _: Throwable =>
29+
throw ex
30+
finally outstream.close()
31+
}
32+
}
33+
34+
def outputPath(parts: List[String], acc: JPath): JPath =
35+
parts match
36+
case Nil => throw new Exception("Invalid class name")
37+
case last :: Nil =>
38+
val name = last.stripSuffix("$")
39+
acc.resolve(s"$name.betasty")
40+
case pkg :: tail =>
41+
val next = acc.resolve(pkg)
42+
if !JFiles.exists(next) then JFiles.createDirectory(next)
43+
outputPath(tail, next)

0 commit comments

Comments
 (0)