LF: check dependencies are not compiled to newer version. (#7050)

This PR prevents a module to depend on an older version of LF than the one is compile to.

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Remy 2020-08-07 18:32:20 +02:00 committed by GitHub
parent 8bb4cccdd8
commit a39b7cc003
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 169 additions and 233 deletions

View File

@ -124,8 +124,11 @@ final class ConcurrentCompiledPackages extends MutableCompiledPackages {
// update the packageMaxLanguageVersions
// If the package is empty, no update
computePackageLanguageVersion(_packagesLanguageVersions, pkg)
.foreach(_packagesLanguageVersions.update(pkgId, _))
pkg.modules.values.headOption
.foreach(
mod =>
// all modules of a package are compiled to the same LF version
_packagesLanguageVersions.update(pkgId, mod.languageVersion))
}
}
}

View File

@ -1,183 +0,0 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf
import java.util.concurrent.atomic.AtomicInteger
import com.daml.lf.data.Ref
import com.daml.lf.data.Ref.PackageId
import com.daml.lf.engine.ConcurrentCompiledPackages
import com.daml.lf.language.Ast.Package
import com.daml.lf.language.{Ast, LanguageVersion, Util => AstUtil}
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.{Matchers, WordSpec}
final class CompiledPackageSpec extends WordSpec with Matchers with TableDrivenPropertyChecks {
val Seq(v1_6, v1_7, v1_8) =
Seq("6", "7", "8").map(minor =>
LanguageVersion(LanguageVersion.Major.V1, LanguageVersion.Minor.Stable(minor)))
private[this] final class ModBuilder(languageVersion: LanguageVersion) {
import ModBuilder._
val name = Ref.ModuleName.assertFromString(s"module${counter.incrementAndGet()}")
private[this] var _deps = Set.empty[PackageId]
private[this] var _body: Ast.Expr = AstUtil.EUnit
private[this] var _built = false
def addDeps(deps: (Ref.PackageId, Ref.ModuleName)): this.type = {
assert(!_built)
val (pkgId, modName) = deps
val id = Ref.Identifier(pkgId, Ref.QualifiedName(modName, noOpName))
_deps = _deps + id.packageId
_body = Ast.ELet(Ast.Binding(None, AstUtil.TUnit, Ast.EVal(id)), _body)
this
}
def build(): (Ast.Module, Set[PackageId]) = {
_built = true
Ast.Module(
name,
Map(noOpName -> Ast.DValue(AstUtil.TUnit, false, _body, false)),
languageVersion,
Ast.FeatureFlags.default
) -> _deps
}
}
private[this] object ModBuilder {
private val counter = new AtomicInteger()
private def noOpName = Ref.DottedName.assertFromString("noOp")
def apply(
languageVersion: LanguageVersion,
deps: (Ref.PackageId, Ref.ModuleName)*,
): ModBuilder =
deps.foldLeft(new ModBuilder(languageVersion))(_.addDeps(_))
}
private[this] final class PkgBuilder {
import PkgBuilder._
val id = Ref.PackageId.assertFromString(s"package${counter.incrementAndGet()}")
private[this] var _modules = List.empty[Ast.Module]
private[this] var _deps = Set.empty[PackageId]
private[this] var _built = false
def addMod(modBuilder: ModBuilder): this.type = {
assert(!_built)
val (mod, deps) = modBuilder.build()
_modules = mod :: _modules
_deps = _deps | deps
this
}
def build: Package = {
_built = true
Package(_modules, _deps, None)
}
}
private[this] object PkgBuilder {
private val counter = new AtomicInteger()
def apply(modBuilders: ModBuilder*): PkgBuilder =
modBuilders.foldLeft(new PkgBuilder())(_.addMod(_))
}
tests("PureCompiledPackages")(PureCompiledPackages(_).toOption.get)
tests("ConcurrentCompiledPackages") { packages =>
val compiledPackages = ConcurrentCompiledPackages()
packages.foreach {
case (pkgId, pkg) =>
compiledPackages
.addPackage(pkgId, pkg)
.consume(
_ => sys.error("unexpected error"),
packages.get,
_ => sys.error("unexpected error"),
)
.toOption
.get
}
compiledPackages
}
def tests(name: String)(_compile: Map[Ref.PackageId, Ast.Package] => CompiledPackages) =
s"$name#getMaxLanguageVersione" should {
def compile(packages: PkgBuilder*) =
_compile(packages.map(b => b.id -> b.build).toMap)
"return None for empty Package" in {
val pkg1 = PkgBuilder()
val compiledPackages = compile(pkg1)
compiledPackages.packageLanguageVersion.lift(pkg1.id) shouldBe None
}
"return None for non defined Package" in {
val compiledPackages = compile()
compiledPackages.packageLanguageVersion.lift(PkgBuilder().id) shouldBe None
}
"return the newest language version of package with unique modules" in {
val pkg = PkgBuilder(ModBuilder(v1_6))
val compiledPackages = compile(pkg)
compiledPackages.packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_6)
}
"return the newest language version of package with several modules" in {
val Seq(mod6, mod7, mod8) = Seq(v1_6, v1_7, v1_8).map(ModBuilder(_))
Seq(mod6, mod7).permutations.foreach { mods =>
val pkg = PkgBuilder(mods: _*)
compile(pkg).packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_7)
}
Seq(mod6, mod7, mod8).permutations.foreach { mods =>
val pkg = PkgBuilder(mods: _*)
compile(pkg).packageLanguageVersion.lift(pkg.id) shouldBe Some(v1_8)
}
}
"return the newest language version of package with several package" in {
val Seq(mod6, mod7, mod8) = Seq(v1_6, v1_7, v1_8).map(ModBuilder(_))
val Seq(pkg6, pkg7, pkg8) = Seq(mod6, mod7, mod8).map(PkgBuilder(_))
val Seq(dep6, dep7, dep8) =
Seq(pkg6.id -> mod6.name, pkg7.id -> mod7.name, pkg8.id -> mod8.name)
val testCases = Table(
("pkgVersion", "dependencies", "output"),
(v1_6, Seq(dep6), v1_6),
(v1_6, Seq(dep7), v1_7),
(v1_6, Seq(dep8), v1_8),
(v1_6, Seq(dep6, dep7), v1_7),
(v1_6, Seq(dep7, dep8), v1_8),
(v1_6, Seq(dep6, dep7, dep8), v1_8),
(v1_7, Seq(dep6), v1_7),
(v1_7, Seq(dep8), v1_8),
(v1_7, Seq(dep7, dep8), v1_8),
(v1_7, Seq(dep6, dep7, dep8), v1_8),
(v1_8, Seq(dep6), v1_8),
(v1_8, Seq(dep7), v1_8),
(v1_8, Seq(dep6, dep7), v1_8),
)
testCases.forEvery {
case (pkgVersion, dependencies, output) =>
val pkg = PkgBuilder(ModBuilder(pkgVersion, dependencies: _*))
compile(pkg, pkg6, pkg7, pkg8).packageLanguageVersion.lift(pkg.id) shouldBe Some(output)
}
}
}
}

View File

@ -27,19 +27,6 @@ abstract class CompiledPackages {
def profilingMode: Compiler.ProfilingMode
def compiler: Compiler = Compiler(packages, stackTraceMode, profilingMode)
// computes the newest language version used in `pkg` and all its dependencies.
// assumes that `maxVersionOfDependencies` is defined for all dependencies of `pkg`
// returns None iff the package is empty.
protected def computePackageLanguageVersion(
dependenciesPackageVersion: PartialFunction[PackageId, LanguageVersion],
pkg: Package,
): Option[LanguageVersion] = {
import transaction.VersionTimeline.maxVersion
val moduleVersions = pkg.modules.values.iterator.map(_.languageVersion)
val dependencyVersions = pkg.directDeps.iterator.map(dependenciesPackageVersion)
(moduleVersions ++ dependencyVersions).reduceOption(maxVersion[LanguageVersion])
}
}
final class PureCompiledPackages private (
@ -54,20 +41,12 @@ final class PureCompiledPackages private (
override def stackTraceMode = stacktracing
override def profilingMode = profiling
private[this] def sortedPkgIds: List[PackageId] = {
val dependencyGraph = packageIds.view
.flatMap(pkgId => getPackage(pkgId).map(pkg => pkgId -> pkg.directDeps).toList)
.toMap
language.Graphs
.topoSort(dependencyGraph)
.getOrElse(throw new IllegalArgumentException("cyclic package definitions"))
}
override val packageLanguageVersion: Map[PackageId, LanguageVersion] =
sortedPkgIds.foldLeft(Map.empty[PackageId, LanguageVersion])(
(acc, pkgId) =>
computePackageLanguageVersion(acc, packages(pkgId)).fold(acc)(acc.updated(pkgId, _))
)
packages.foldLeft(Map.empty[PackageId, LanguageVersion]) {
case (acc, (pkgId, pkg)) =>
// all modules of a package are compiled to the same LF version
pkg.modules.values.headOption.fold(acc)(mod => acc.updated(pkgId, mod.languageVersion))
}
}
object PureCompiledPackages {

View File

@ -1635,6 +1635,8 @@ Then, a collection of packages ``Ξ`` is well-formed if:
package of ``Ξ``.
* There are no cycles between type synonym definitions, modules, and
packages references.
* Each package ``p`` only depends on packages whose LF version is older
than or the same as the LF version of ``p`` itself.
Operational semantics

View File

@ -20,6 +20,7 @@ da_scala_library(
deps = [
"//daml-lf/data",
"//daml-lf/language",
"//daml-lf/transaction",
"@maven//:org_scalaz_scalaz_core_2_12",
],
)
@ -34,5 +35,6 @@ da_scala_test(
"//daml-lf/data",
"//daml-lf/language",
"//daml-lf/parser",
"@maven//:org_scalaz_scalaz_core_2_12",
],
)

View File

@ -10,8 +10,8 @@ import com.daml.lf.validation.Util._
private[validation] object Collision {
def checkPackage(pkgId: PackageId, modules: Traversable[(ModuleName, Ast.Module)]): Unit = {
val entitiesMap = namedEntitiesFromPkg(modules).groupBy(_.fullyResolvedName)
def checkPackage(pkgId: PackageId, pkg: Ast.Package): Unit = {
val entitiesMap = namedEntitiesFromPkg(pkg.modules).groupBy(_.fullyResolvedName)
entitiesMap.values.foreach(cs => checkCollisions(pkgId, cs.toList))
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.validation
import com.daml.lf.data.Ref._
import com.daml.lf.language.Ast._
import com.daml.lf.transaction.VersionTimeline
private[validation] object DependencyVersion {
@throws[ValidationError]
def checkPackage(world: World, pkgId: PackageId, pkg: Package): Unit = {
import VersionTimeline.Implicits._
for {
pkgFirstModule <- pkg.modules.values.take(1)
// all modules of a package are compiled to the same LF version
pkgLangVersion = pkgFirstModule.languageVersion
depPkgId <- pkg.directDeps
depPkg = world.lookupPackage(NoContext, depPkgId)
depFirstModule <- depPkg.modules.values.take(1)
depLangVersion = depFirstModule.languageVersion
} if (pkgLangVersion precedes depLangVersion)
throw EModuleVersionDependencies(
pkgId,
pkgLangVersion,
depPkgId,
depLangVersion
)
}
}

View File

@ -13,14 +13,14 @@ private[validation] object Recursion {
/* Check there are no cycles in the module references */
@throws[ValidationError]
def checkPackage(pkgId: PackageId, modules: Map[ModuleName, Module]): Unit = {
val g = modules.map {
def checkPackage(pkgId: PackageId, pkg: Package): Unit = {
val g = pkg.modules.map {
case (name, mod) => name -> (mod.definitions.values.flatMap(modRefs(pkgId, _)).toSet - name)
}
Graphs.topoSort(g).left.foreach(cycle => throw EImportCycle(NoContext, cycle.vertices))
modules.foreach { case (modName, mod) => checkModule(pkgId, modName, mod) }
pkg.modules.foreach { case (modName, mod) => checkModule(pkgId, modName, mod) }
}
def modRefs(pkgId: PackageId, definition: Definition): Set[ModuleName] = {

View File

@ -21,17 +21,18 @@ object Validation {
): Either[ValidationError, Unit] =
runSafely {
val world = new World(pkgs)
unsafeCheckPackage(world, pkgId, world.lookupPackage(NoContext, pkgId).modules)
unsafeCheckPackage(world, pkgId, world.lookupPackage(NoContext, pkgId))
}
private def unsafeCheckPackage(
world: World,
pkgId: PackageId,
modules: Map[ModuleName, Module]
pkg: Package
): Unit = {
Collision.checkPackage(pkgId, modules)
Recursion.checkPackage(pkgId, modules)
modules.values.foreach(unsafeCheckModule(world, pkgId, _))
Collision.checkPackage(pkgId, pkg)
Recursion.checkPackage(pkgId, pkg)
DependencyVersion.checkPackage(world, pkgId, pkg)
pkg.modules.values.foreach(unsafeCheckModule(world, pkgId, _))
}
def checkModule(

View File

@ -6,6 +6,7 @@ package com.daml.lf.validation
import com.daml.lf.data.ImmArray
import com.daml.lf.data.Ref._
import com.daml.lf.language.Ast._
import com.daml.lf.language.LanguageVersion
sealed abstract class LookupError extends Product with Serializable {
def pretty: String
@ -367,3 +368,20 @@ final case class ECollision(
override protected def prettyInternal: String =
s"collision between ${entity1.pretty} and ${entity2.pretty}"
}
final case class EModuleVersionDependencies(
pkgId: PackageId,
pkgLangVersion: LanguageVersion,
depPkgId: PackageId,
dependencyLangVersion: LanguageVersion,
) extends ValidationError {
import com.daml.lf.transaction.VersionTimeline.Implicits._
assert(pkgId != depPkgId)
assert(pkgLangVersion precedes dependencyLangVersion)
override protected def prettyInternal: String =
s"package $pkgId using version $pkgLangVersion depends on package $depPkgId using newer version $dependencyLangVersion"
override def context: Context = NoContext
}

View File

@ -11,7 +11,7 @@ import org.scalatest.{Matchers, WordSpec}
class CollisionSpec extends WordSpec with Matchers {
def check(pkg: Package): Unit =
Collision.checkPackage(defaultPackageId, pkg.modules)
Collision.checkPackage(defaultPackageId, pkg)
"Collision validation" should {

View File

@ -0,0 +1,85 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.lf.validation
import com.daml.lf.data.Ref.{DottedName, Identifier, PackageId, QualifiedName}
import com.daml.lf.language.Ast._
import com.daml.lf.language.Util._
import com.daml.lf.language.{LanguageVersion => LV}
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.{Matchers, WordSpec}
class DependencyVersionSpec extends WordSpec with TableDrivenPropertyChecks with Matchers {
private[this] val v1_6 = LV(LV.Major.V1, LV.Minor.Stable("6"))
private[this] val v1_7 = LV(LV.Major.V1, LV.Minor.Stable("7"))
private[this] val v1_8 = LV(LV.Major.V1, LV.Minor.Stable("8"))
private[this] val A = (PackageId.assertFromString("-pkg1-"), DottedName.assertFromString("A"))
private[this] val B = (PackageId.assertFromString("-pkg2-"), DottedName.assertFromString("B"))
private[this] val E = (PackageId.assertFromString("-pkg3-"), DottedName.assertFromString("E"))
private[this] val u = DottedName.assertFromString("u")
"Dependency validation should detect cycles between modules" in {
def pkg(
ref: (PackageId, DottedName),
langVersion: LV,
depRefs: (PackageId, DottedName)*,
) = {
val (pkgId, modName) = ref
val mod = Module(
modName,
(
(u -> DValue(TUnit, true, EUnit, false)) +:
depRefs.map {
case (depPkgId, depModName) =>
depModName -> DValue(
TUnit,
true,
EVal(Identifier(depPkgId, QualifiedName(depModName, u))),
false)
}
),
langVersion,
FeatureFlags.default
)
pkgId -> Package(Map(modName -> mod), depRefs.iterator.map(_._1).toSet - pkgId, None)
}
val negativeTestCases = Table(
"valid packages",
Map(pkg(A, v1_8, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_6, E)),
Map(pkg(A, v1_8, A, B, E), pkg(B, v1_8, B, E), pkg(E, v1_6, E)),
)
val postiveTestCase = Table(
("invalid module", "packages"),
A -> Map(pkg(A, v1_6, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_6, E)),
A -> Map(pkg(A, v1_7, A, B, E), pkg(B, v1_7, B, E), pkg(E, v1_8, E)),
B -> Map(pkg(A, v1_8, A, B, E), pkg(B, v1_6, B, E), pkg(E, v1_7, E)),
)
forEvery(negativeTestCases) { pkgs =>
pkgs.foreach {
case (pkgId, pkg) =>
DependencyVersion.checkPackage(new World(pkgs), pkgId, pkg)
}
}
forEvery(postiveTestCase) {
case ((pkgdId, _), pkgs) =>
val world = new World(pkgs)
an[EModuleVersionDependencies] should be thrownBy
DependencyVersion.checkPackage(
world,
pkgdId,
world.lookupPackage(NoContext, pkgdId),
)
}
}
}

View File

@ -22,7 +22,7 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
}
"""
Recursion.checkPackage(defaultPackageId, p.modules)
Recursion.checkPackage(defaultPackageId, p)
}
@ -61,11 +61,9 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
${module("E", "E")}
"""
Recursion.checkPackage(defaultPackageId, negativeCase.modules)
an[EImportCycle] should be thrownBy
Recursion.checkPackage(defaultPackageId, positiveCase1.modules)
an[EImportCycle] should be thrownBy
Recursion.checkPackage(defaultPackageId, positiveCase2.modules)
Recursion.checkPackage(defaultPackageId, negativeCase)
an[EImportCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase1)
an[EImportCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase2)
}
@ -98,11 +96,9 @@ class RecursionSpec extends WordSpec with TableDrivenPropertyChecks with Matcher
}
"""
Recursion.checkPackage(defaultPackageId, negativeCase.modules)
an[ETypeSynCycle] should be thrownBy
Recursion.checkPackage(defaultPackageId, positiveCase1.modules)
an[ETypeSynCycle] should be thrownBy
Recursion.checkPackage(defaultPackageId, positiveCase2.modules)
Recursion.checkPackage(defaultPackageId, negativeCase)
an[ETypeSynCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase1)
an[ETypeSynCycle] should be thrownBy Recursion.checkPackage(defaultPackageId, positiveCase2)
}