Draw the boundary once: the scorecard

This page gives the evidence for the scores in Draw the boundary once. Each table gives the score, the reason and a source for one pair of compiler and build system. The scores are from 25 October 2026.

The six tests come from the equation in the post:

  1. Function: the compiler and the build system read only declared inputs.
  2. Interface: when the compiler compiles a package, it reads only the interface files of the dependencies. No part of an interface file comes from a function body.
  3. Cutoff: a dependency changes only a private item, or the body of a function that is not generic or inline. Then the build system compiles no other package again.
  4. Graph: the build system gets the package graph without a scan of the source files.
  5. Visibility: what is visible does not depend on which files the build system compiles together.
  6. Bump: the package manager calculates the version bump from the difference between the two interfaces.

✓ passes, ◐ passes partially, ✗ fails, – does not apply. No pair of the 16 passes all six tests.

Elm (elm make, elm publish): 4 of 6

TestScoreReasonSource
Function◐Elm has no compile-time code, and packages cannot use ports or kernel (JavaScript) code. When Elm builds a package, it selects the dependency versions from the registry, and it uses no lock file.Parse/Module.hs, Elm/Details.hs, ports guide
Interface✓Package documentation refuses exposing (..), and it refuses an exposed value that has no type annotation. A module inside an application can still have an interface that Elm infers.Elm/Docs.hs, Error/Docs.hs
Cutoff✓The build compares the new .elmi file with the old one. When they are the same, the build keeps the dependents in its cache. The .elmi file holds only exposed values.Build.hs, Interface.hs
Graph◐elm.json gives the package graph, but the build parses each source file to find its imports.Build.hs
Visibility✓The exposing list of the module and exposed-modules set visibility. Two files for one module name cause an error.Interface.hs
Bump✓elm publish compares the old interface with the new interface, and it refuses a version number that does not agree.Publish.hs

Java + Bazel: 4 of 6

TestScoreReasonSource
Function◐The sandbox shows an action only its declared inputs. But processes can still read the file system of the host, and Bazel allows network access by default.Sandboxing, command-line reference
Interface✓Dependents compile against header jars. Turbine and ijar remove the method code and the private members.Turbine RemovePrivateMembers, ijar README
Cutoff✓Skyframe skips the dependents when a value that it built again did not change. So Bazel compiles A again only when the header jar of B changed.Skyframe, compiler-dev mail
Graph✓The deps lists in BUILD files give the graph.Java rules
Visibility✓The Java source sets access. The visibility attribute of Bazel adds a declared gate at the target level.Visibility
Bump✗Bazel modules have versions, but the documentation says that the meaning of a version increase is “not enforced”.Modules

Java + Gradle: 3 of 6

TestScoreReasonSource
Function✗Build scripts are code that can read environment variables and files, and Gradle does not put tasks in a sandbox.Configuration cache, incremental build
Interface◐Java signatures state all types, but javac reads the full class files of the dependency.Java library plugin
Cutoff✓Compile avoidance ignores method bodies and private members. The hash still includes package-private members and constants.Compile avoidance
Graph✓Gradle gets the graph from declared dependencies, although the build file is code.Build lifecycle
Visibility✓The Java source sets access. The build file declares api and implementation.JLS 6.6
Bump◐Only japicmp, a tool from another project, suggests the bump.japicmp

Rust + Cargo: 2 of 6

TestScoreReasonSource
Function✗build.rs and procedural macros can read any file or environment variable. The interface that records these reads is available only in nightly Rust.Build scripts, proc_macro::tracked
Interface✗Auto traits such as Send pass through -> impl Trait and async fn, so the body decides a part of the interface.RFC 1522
Cutoff✗The fingerprint of a crate includes the input hashes of its dependencies. So each change in a dependency causes its dependents to compile again. Early cutoff is an open issue.fingerprint/mod.rs, cargo#17439
Graph✓Cargo.toml and Cargo.lock give the graph. Cargo still finds targets such as src/bin/* from the directory layout.Specifying dependencies, targets
Visibility✓Visibility follows the module tree that the mod declarations make, from the crate root.Visibility and privacy
Bump◐Only cargo-semver-checks, a separate tool, checks the bump. A 2026 project goal is to move it into cargo publish.Project goal

Rust + Buck2: 2 of 6

TestScoreReasonSource
Function◐Buck2 runs build.rs as an action with an explicit environment, but it does not put local actions in a sandbox.cargo_buildscript.bzl, Buck2
Interface✗Buck2 uses the same rustc metadata as Cargo.build_params.bzl
Cutoff✗The DICE engine has early cutoff, but the .rmeta file changes at each source change, so the cutoff cannot occur. A test with rustc 1.95.0 showed this for the body of a private function.DICE incrementality
Graph✓BUCK files declare the dependencies. Reindeer generates them from Cargo.toml.Buck2
Visibility✓The same Rust module tree as Cargo.Visibility and privacy
Bump✗Buck2 and Reindeer have no semantic-version step.Reindeer manual

Rust + Bazel (rules_rust): 2 of 6

TestScoreReasonSource
Function◐Bazel runs actions in a sandbox, but it allows network access by default, and build scripts get the default shell environment.Sandboxing, rules_rust settings
Interface✗rules_rust uses the same rustc metadata as Cargo.RFC 1522
Cutoff✗Pipelined compilation is off by default, so dependents read the full .rlib file. When it is on, the .rmeta file still changes at each change.Skyframe, rules_rust settings
Graph✓BUILD files declare each direct dependency.Dependencies
Visibility✓The same Rust module tree, and the srcs list names each source file.Visibility and privacy
Bump✗rules_rust has no semantic-version feature.Modules

C++ headers + Bazel: 1 of 6

TestScoreReasonSource
Function◐The same sandbox limits as Java + Bazel: actions can read the file system of the host, and Bazel allows network access by default.Sandboxing
Interface✗Dependents compile the headers, which hold private members and inline and template bodies. The body decides an auto return type.C/C++ rules, cppreference: function
Cutoff◐A change to a .cc file does not cause dependents to compile again. Each change to a header causes this, also a change to a private member.Skyframe, command-line reference
Graph✓BUILD files declare headers in hdrs and srcs. Include scanning is off by default.C/C++ rules
Visibility◐To keep srcs headers private, Bazel needs the layering_check of clang, and that check covers only direct includes.C/C++ rules
Bump✗The same as Java + Bazel.Modules

C++20 modules + CMake and Ninja: 0 of 6

TestScoreReasonSource
Function✗Ninja has no sandbox, and it finds headers from the depfiles of the compiler, by the time of each file. CMake gets the compiler from the CXX environment variable.Ninja manual, CMake: CXX
Interface◐Importers read a compiled module interface (BMI), but Clang may open the input files of a BMI. Reduced BMIs, without non-inline bodies, became the default only in Clang 22.Clang: C++ modules
Cutoff◐The CMake documentation says that compilers write the BMI again at each compilation. So each change to an interface unit causes all importers to compile again. An implementation unit has no BMI.cmake-cxxmodules
Graph✗CMake scans the source files during the build (P1689 format) and gives the result to Ninja.cmake-cxxmodules
Visibility◐The source declares export. But Clang alone decides whether internal partitions are reachable, and CMake does not check private module sets.Clang: C++ modules, cmake-cxxmodules
Bump✗The author selects the compatibility mode manually.CMakePackageConfigHelpers

Kotlin + Gradle: 1 of 6

TestScoreReasonSource
Function✗The same as Java + Gradle: build scripts are code, and Gradle does not put tasks in a sandbox.Incremental build
Interface✗A single-expression function gets its return type from the body, unless explicit API mode is on.Functions, What's new in 1.4
Cutoff◐Gradle starts the Kotlin compiler after each change in a dependency. Then ABI snapshots limit what compiles again. Inline bodies and const val values are part of the ABI.JetBrains blog, 2022
Graph✓The same as Java + Gradle.Build lifecycle
Visibility✗internal means “a set of Kotlin files compiled together”, for example a Gradle source set.Visibility modifiers
Bump◐The experimental ABI validation fails when the ABI changes, but it ignores the version number.Binary compatibility validation

Go + the go command: 0 of 6

TestScoreReasonSource
Function◐Go has no build scripts or compile-time code. But cgo reads CC, CGO_CFLAGS and pkg-config, and the go command finds go.work from the current directory.cgo, Modules reference
Interface◐Dependents read export data. It holds the bodies of inlinable functions and generic functions, and of the unexported helpers that they use.noder/linker.go, generics design
Cutoff✗Go 1.27 keys each dependent on a hash of the full archive, which includes object code. A key on export data came to the development branch in August 2026, and no release has it yet. Export data also records line positions.exec.go, Go 1.27, commit 1dacc62658, go#15752
Graph✗The package graph comes from the import lines of each source file. go.mod gives only the module requirements.imports/read.go
Visibility✗The specification lets the tool decide which files are in a package, and internal/ is a rule about directory paths.Go spec, cmd/go
Bump◐gorelease suggests a version, but it is experimental and nothing enforces it.gorelease

Zig + zig build: 1 of 6

TestScoreReasonSource
Function✗build.zig can be any code, with access to the environment and the file system. The compiler itself is pure: comptime cannot call external functions.std/Build.zig, 0.17.0 release notes
Interface✗A project is one compilation of all modules from source. An inferred error set gets a part of the return type from the body.Language reference
Cutoff✗By default, each change runs the full compilation again. Incremental compilation for each declaration needs -fincremental.Compilation.zig, 0.17.0 release notes
Graph◐build.zig.zon lists packages as data, but the module graph exists only after build.zig runs.build.zig.zon
Visibility✓A declaration without pub stays in its file, and files join only through @import.import outside module path
Bump✗version is a string that nothing checks.build.zig.zon

Nix: 1 of 6

TestScoreReasonSource
Function◐On Linux, a build sees only its store inputs and has no network access. Fixed-output derivations keep network access, and the sandbox is off by default on other systems.nix.conf
Interface✗Inputs are the full outputs of other derivations. There is no interface.Derivations
Cutoff✗An output path includes a hash of the derivation of each dependency, so each change causes each dependent to build again. Content-addressed derivations are experimental, and they need outputs that are identical bit for bit.Input addressing, experimental features
Graph✓Each derivation declares all of its inputs. Import-from-derivation is the exception.Derivations, import from derivation
Visibility–Nix has no item visibility. The compiler that Nix runs decides.Derivations
Bump–A hash pins each dependency, and there are no version ranges.Derivations

OCaml + dune: 0 of 6

TestScoreReasonSource
Function✗The file inputs of a preprocessor count only if the author lists them. The dune sandbox is optional, and it keeps the environment variables.Sandboxing, preprocessing
Interface◐Dependents read the .cmi file, which can come from an .mli file that a person wrote. Without an .mli file, the compiler infers the types. Native code also reads the .cmx file, unless the build uses -opaque.Batch compiler, native compiler
Cutoff◐The dev profile uses -opaque and content hashes. The release profile, which opam uses, does not use -opaque. A .cmi file also records the digests of all interfaces that it imported.Profiles, persistent_env.ml
Graph◐The libraries field gives the library graph, but ocamldep scans the source files to find module dependencies.ocamldep, Discuss report
Visibility◐The .mli file sets visibility. But by default, a library holds each module file in its directory.library stanza
Bump✗opam has no semantic-version rule and no interface check.opam manual

Haskell GHC + Cabal: 0 of 6

TestScoreReasonSource
Function✗Template Haskell runIO can do any IO at compile time, and it is optional to declare the files that it reads. A custom Setup.hs can run any code.Template Haskell, package description
Interface◐Top-level type signatures are optional, and above -O0 the .hi file holds function bodies.Warnings, optimisation
Cutoff◐GHC keeps a fingerprint for each declaration. But Cabal builds with -O1 by default, so a change to a body gets to the dependents.Separate compilation, cabal.project
Graph◐build-depends gives the package graph. ghc --make follows the imports to find modules.Separate compilation
Visibility◐Export lists set visibility, but GHC can use a module from disk that the package does not list.Warnings
Bump✗The PVP gives the rules, but Hackage does not check them.PVP

Swift + SwiftPM: 0 of 6

TestScoreReasonSource
Function✗Package.swift can read each environment variable and the git status through an official interface. The macOS sandbox allows all file reads.Context.swift, Sandbox.swift
Interface◐Dependents read a .swiftmodule file. Library evolution is off by default, so clients see each layout. The body decides the concrete type behind some P.SE-0260, SE-0244
Cutoff◐Debug builds use fingerprints across modules. Release builds compile each module as one unit, and a change to a private stored property changes the interface.swift-driver, SwiftModuleBuildDescription
Graph◐Targets come from the manifest. But an import that the manifest does not declare compiles if an earlier build left the module.Options.swift, forum thread
Visibility✗internal means the module, and the module is the set of files that the build target holds.Access control, Target.swift
Bump◐swift package diagnose-api-breaking-changes exists. It is optional, and it reports breaks but does not calculate the bump.Documentation

Vx today: 0 of 6

This table uses the compiler source on main at commit e66345d3. The post describes the design. This table describes what exists.

TestScoreReasonSource
Function✗The compiler reads 19 environment variables, and 15 of them can change the program it builds or the tool that builds it. It finds z3 and the LLVM tools through PATH. Without VX_STD_PATH, it looks for the standard library next to the compiler, then in the current directory.module_loader.rs, jit.rs, solver.rs, VxLowering.cpp
Interface◐A signature is the full contract, and vxc --emit-interface writes a .vxlib interface file. The file also holds function bodies, and the parallel compiler (-j) does not read it yet.driver.rs
Cutoff✗There is no build system yet, so nothing skips a compilation.
Graph◐Each package comes from a --package name=dir flag. There is no manifest yet.driver.rs
Visibility◐pub items and private items exist, but pkg and ws do not exist yet. Imports use search paths that include the current directory.module_loader.rs
Bump✗There is no package manager yet.

Back to Draw the boundary once.