File: README.md
Carbon Language: An experimental successor to C++
See our announcement video from
CppNorth. Note that Carbon is
not ready for use.
Fast and works with C++
- Performance matching C++ using LLVM, with low-level access to bits and
addresses - Interoperate with your existing C++ code, from inheritance to templates
- Fast and scalable builds that work with your existing C++ build systems
Modern and evolving
- Solid language foundations that are easy to learn, especially if you have
used C++ - Easy, tool-based upgrades between Carbon versions
- Safer fundamentals, and an incremental path towards a memory-safe subset
Welcoming open-source community
- Clear goals and priorities with robust governance
- Community that works to be welcoming, inclusive, and friendly
- Batteries-included approach: compiler, libraries, docs, tools, package
manager, and more
Why build Carbon?
C++ remains the dominant programming language for performance-critical software,
with massive and growing codebases and investments. However, it is struggling to
improve and meet developers' needs, as outlined above, in no small part due to
accumulating decades of technical debt. Incrementally improving C++ is
extremely difficult, both due to
the technical debt itself and challenges with its evolution process. The best
way to address these problems is to avoid inheriting the legacy of C or C++
directly, and instead start with solid language foundations like
modern generics system, modular code organization, and consistent,
simple syntax.
Existing modern languages already provide an excellent developer experience: Go,
Swift, Kotlin, Rust, and many more. Developers that can use one of these
existing languages should. Unfortunately, the designs of these languages
present significant barriers to adoption and migration from C++. These barriers
range from changes in the idiomatic design of software to performance overhead.
Carbon is fundamentally a successor language approach, rather than an
attempt to incrementally evolve C++. It is designed around interoperability with
C++ as well as large-scale adoption and migration for existing C++ codebases and
developers. A successor language for C++ requires:
- Performance matching C++, an essential property for our developers.
- Seamless, bidirectional interoperability with C++, such that a library
anywhere in an existing C++ stack can adopt Carbon without porting the rest. - A gentle learning curve with reasonable familiarity for C++ developers.
- Comparable expressivity and support for existing software's design and
architecture. - Scalable migration, with some level of source-to-source translation for
idiomatic C++ code.
With this approach, we can build on top of C++'s existing ecosystem, and bring
along existing investments, codebases, and developer populations. There are a
few languages that have followed this model for other ecosystems, and Carbon
aims to fill an analogous role for C++:
- JavaScript โ TypeScript
- Java โ Kotlin
- C++ โ Carbon
Language Goals
We are designing Carbon to support:
- Performance-critical software
- Software and language evolution
- Code that is easy to read, understand, and write
- Practical safety and testing mechanisms
- Fast and scalable development
- Modern OS platforms, hardware architectures, and environments
- Interoperability with and migration from existing C++ code
While many languages share subsets of these goals, what distinguishes Carbon is
their combination.
We also have explicit non-goals for Carbon, notably including:
- A stable
application binary interface
(ABI) for the entire language and library - Perfect backwards or forwards compatibility
Our detailed goals document fleshes out these ideas
and provides a deeper view into our goals for the Carbon project and language.
Project status
Carbon Language is currently an experimental project. We are hard at work on a
toolchain implementation with compiler and linker. You can try out the current
state at compiler-explorer.com.
We want to better understand whether we can build a language that meets our
successor language criteria, and whether the resulting language can gather a
critical mass of interest within the larger C++ industry and community.
Currently, we have fleshed out several core aspects of both Carbon the project
and the language:
- The strategy of the Carbon Language and project.
- An open-source project structure, governance model, and evolution process.
- Critical and foundational aspects of the language design informed by our
experience with C++ and the most difficult challenges we anticipate. This
includes designs for:- Generics
- Class types
- Inheritance
- Operator overloading
- Lexical and syntactic structure
- Code organization and modular structure
- An under-development compiler and toolchain that will compile
Carbon (and eventually C++ code as well) into standard executable code. This
is where most of our current implementation efforts are directed.- Historically, there was also a prototype
explorer interpreter that
implemented an older version of the Carbon language design, but is no
longer under development and has been archived.
- Historically, there was also a prototype
If you're interested in contributing, we're currently focused on developing the
Carbon toolchain until it can
support Carbon โ C++ interop.
Beyond that, we plan to continue developing the design and toolchain until we
can ship the
0.1 language
and support evaluating Carbon in more detail.
You can see our full roadmap for more details.
Carbon and C++
If you're already a C++ developer, Carbon should have a gentle learning curve.
It is built out of a consistent set of language constructs that should feel
familiar and be easy to read and understand.
The Carbon code here is hypothetical and meant to show the look and feel of the
language.
C++ code like this:
corresponds to this Carbon code:
You can call Carbon from C++ without overhead and the other way around. This
means you migrate a single C++ library to Carbon within an application, or write
new Carbon on top of your existing C++ investment. For example:
Read more about
C++ interop in Carbon.
Beyond interoperability between Carbon and C++, we're also planning to support
migration tools that will mechanically translate idiomatic C++ code into Carbon
code to help you switch an existing C++ codebase to Carbon.
Generics
Carbon provides a
modern generics system
with checked definitions, while still supporting opt-in
templates for seamless C++ interop. Checked
generics provide several advantages compared to C++ templates:
- Generic definitions are fully type-checked, removing the need to
instantiate to check for errors and giving greater confidence in code.- Avoids the compile-time cost of re-checking the definition for every
instantiation. - When using a definition-checked generic, usage error messages are
clearer, directly showing which requirements are not met.
- Avoids the compile-time cost of re-checking the definition for every
- Enables automatic, opt-in type erasure and dynamic dispatch without a
separate implementation. This can reduce the binary size and enables
constructs like heterogeneous containers. - Strong, checked interfaces mean fewer accidental dependencies on
implementation details and a clearer contract for consumers.
Without sacrificing these advantages, Carbon generics support
specialization, ensuring it can fully address performance-critical use cases
of C++ templates. For more details about Carbon's generics, see their
design.
In addition to easy and powerful interop with C++, Carbon templates can be
constrained and incrementally migrated to checked generics at a fine granularity
and with a smooth evolutionary path.
Memory safety
Safety, and especially
memory safety, remains a key
challenge for C++ and something a successor language needs to address.
We plan to support a two step migration process:
- Highly automated, minimal supervision migration from C++ to a dialect of
Carbon designed for C++ interop and migration. - Incremental refactoring of the Carbon code to adopt memory-safe designs,
patterns, and APIs.
We also want to address important, low-hanging fruit in the safety space
immediately when migrating into Carbon:
- Tracking uninitialized states better, increased enforcement of
initialization, and hardening against initialization bugs when needed. - Designing fundamental APIs and idioms to support dynamic bounds checking.
- Switching from undefined behavior to erroneous behavior wherever possible,
and marking the remaining undefined behavior with visibleunsafesyntax. - Having a default debug build mode that has less runtime overhead while being
more comprehensive than existing C++ debug build modes combined with
Address Sanitizer.
For more details, see our safety design.
Getting started
To try out Carbon immediately in your browser, you can use the toolchain at:
carbon.compiler-explorer.com.
We are developing a traditional toolchain for Carbon that can compile and link
programs. However, Carbon is still an early, experimental project, and so we
only have very experimental nightly releases of the Carbon toolchain available
to download, and only on limited platforms. If you are using a recent Ubuntu
Linux or similar (Debian, WSL, etc.), you can try these out by going to our
releases page and
download the latest nightly toolchain tar file:carbon_toolchain-0.0.0-0.nightly.YYYY.MM.DD.tar.gz. Then you can try it out:
# A variable with the nightly version from yesterday:
VERSION="$(date -d yesterday +0.0.0-0.nightly.%Y.%m.%d)"
# Get the release
wget https://github.com/carbon-language/carbon-lang/releases/download/v${VERSION}/carbon_toolchain-${VERSION}.tar.gz
# Unpack the toolchain:
tar -xvf carbon_toolchain-${VERSION}.tar.gz
# Create a simple Carbon source file:
echo "import Core library \"io\"; fn Run() { Core.Print(42); }" > forty_two.carbon
# Compile to an object file:
./carbon_toolchain-${VERSION}/bin/carbon compile \
--output=forty_two.o forty_two.carbon
# Install minimal system libraries used for linking. Note that installing `gcc`
# or `g++` for compiling C/C++ code with GCC will also be sufficient, these are
# just the specific system libraries Carbon linking still uses.
sudo apt install libgcc-11-dev
# Link to an executable:
./carbon_toolchain-${VERSION}/bin/carbon link \
--output=forty_two forty_two.o
# Run it:
./forty_twoAs a reminder, the toolchain is still very early and many things don't yet work.
Please hold off on filing lots of bugs: we know many parts of this don't work
yet or may not work on all systems. We expect to have releases that are much
more robust and reliable that you can try out when we reach our
0.1 milestone.
If you want to build Carbon's toolchain yourself or are thinking about
contributing fixes or improvements to Carbon, you'll need to install our
build dependencies (Clang,
LLD, libc++) and check out the Carbon repository. For example, on Debian or
Ubuntu:
# Update apt.
sudo apt update
# Install tools.
sudo apt install \
clang \
libc++-dev \
libc++abi-dev \
lld
# Download Carbon's code.
$ git clone https://github.com/carbon-language/carbon-lang
$ cd carbon-langThen you can try out our toolchain which has a very early-stage compiler for
Carbon:
# Build and run the toolchain's help to get documentation on the command line.
$ ./scripts/run_bazelisk.py run //toolchain -- helpFor complete instructions, including installing dependencies on various
different platforms, see our
contribution tools documentation.
Learn more about the Carbon project:
Conference talks
Carbon focused talks from the community:
2026
- Carbon memory safety: a first deep dive (July 10,
video,
transcript,
slides) - Benchmarking and optimizing the Carbon compiler, NDC {Toronto} (May 5-8)
(video,
slides) - Carbon: graduating from the experiment, NDC {Toronto} (May 5-8)
(video,
slides)
2025
- Carbon: from C++ to Memory Safety, REBASE - ICFP/SPLASH
(slides) - Memory safety everywhere with both Carbon and Rust, RustConf
(video,
slides)
2024
- Generic implementation strategies in Carbon and Clang, LLVM Developers'
Meeting (video,
slides) - The Carbon Language: Road to 0.1, NDC {TechTown}
(video,
slides) - How designing Carbon with C++ interop taught me about C++ variadics and
overloads, CppNorth (video,
slides) - Generic Arity: Definition-Checked Variadics in Carbon, C++Now
(video,
slides) - Carbon: An experiment in different tradeoffs, panel session, EuroLLVM
(video,
slides) - Carbon's high-level semantic IR lightning talk, EuroLLVM
(video)
2023
- Carbonโs Successor Strategy: From C++ interop to memory safety, C++Now
(video,
slides) - Definition-Checked Generics, C++Now
- Modernizing Compiler Design for Carbonโs Toolchain, C++Now
(video,
slides)
2022
- Carbon Language: Syntax and trade-offs, Core C++
(video,
slides) - Carbon Language: An experimental successor to C++, CppNorth
(video,
slides)
Other videos
We additionally have toolchain videos.
Join us
We'd love to have folks join us and contribute to the project. Carbon is
committed to a welcoming and inclusive environment where everyone can
contribute.
- Most of Carbon's design discussions occur on
Discord. - To watch for major release announcements, subscribe to our
Carbon release post on GitHub
and star carbon-lang. - See our code of conduct and
contributing guidelines for information about the Carbon
development community.
Contributing
You can also directly:
- Contribute to the language design:
feedback on design, new design proposal - Contribute to the language implementation
- Carbon Toolchain, and project infrastructure
You can check out some
"good first issues",
or join the #contributing-help channel on
Discord. See our fullCONTRIBUTING documentation for more details.
File: docs/project/principles/error_handling.md
Principle: Errors are values
Table of contents
Background
Most nontrivial programs contain functions that can fail, meaning that even if
all their preconditions are met, they may not be able to perform their primary
behavior. For example, a function that reads data from a remote server may fail
if the server is unreachable, and a function that parses a string to return an
integer may fail if the input string is not a properly-formatted integer.
In many cases, the function author wants these failures to be recoverable,
meaning that a direct or transitive caller can respond to the failure in some
way that enables the program to continue running.
Principle
A Carbon function that needs to report recoverable failures should return a sum
type whose alternatives represent the success case and failure cases, such asOptional(T), Result(T, Error), or bool. The function's successful return
value, and any metadata about the failure, should be embedded in the
alternatives of the sum type, rather than reported by way of output parameters
or other side channels. Carbon's design will prioritize making this form of
error handling efficient and ergonomic.
Applications of these principles
Carbon errors, unlike exceptions in C++ and similar languages, will not be
propagated implicitly. Instead, Carbon will very likely need to provide some
explicit but syntactically lightweight means of propagating errors, such as
Rust's ? operator, so that error-propagation boilerplate doesn't make it hard
for readers to follow the logic of the success path.
Carbon will not have a special syntax for specifying what kind of errors a
function can emit, such as noexcept or
dynamic exception specifications
in C++, or throws in Java, because that information will be embedded in the
function's return type. Similarly, Carbon errors will be statically typed,
because Carbon return values are statically typed.
File: docs/project/principles/information_accumulation.md
Principle: Information accumulation
Table of contents
Background
There are many different sources of information in a program, and a tool or a
human interpreting code will not in general have full information, but will
still want to draw conclusions about the code.
Different languages take different approaches to this problem. For example:
- In C, information is accumulated linearly in each source file independently,
and only information from earlier in the same file is available. A program
can observe that information is incomplete at one point and complete at
another. - In C++, the behavior is largely similar to C, except:
- Within certain contexts in a class, information from later in the class
definition is available. - With C++20 modules, information from other source files can be made
available. - It is easier to observe -- perhaps even accidentally -- that information
is accumulated incrementally.
- Within certain contexts in a class, information from later in the class
- In Rust, all information from the entire crate is available everywhere
within that crate, with exceptions for constructs like proc macros that can
see the state of the program being incrementally built. - In Swift, all information from the entire source file is available within
that source file.
Principle
In Carbon, information is accumulated incrementally within each source file.
Carbon programs are invalid if they would have a different meaning if more
information were available.
Carbon source files can be interpreted top-down, without referring to
information that appears substantially later in a file. Source files are
expected to be organized into a topological order where that makes sense, with
forward declarations used to introduce names before they are first referenced
when necessary.
If a program attempts to use information that has not yet been provided, the
program is invalid. There are multiple options for how this can be reported:
- The program can be rejected as soon as it tries to use information that
might not be known yet. - For the case where the information can only be provided in the same source
file, an assumption about the information can be made at the point where it
is needed, and the program can be rejected only if that assumption turns out
to be incorrect.
Disallowing programs from changing meaning in the context of more information
ensures that the program is interpreted consistently or is rejected. This is
especially important to the coherence of generics and templates.
Applications of this principle
- As in C++, and unlike in Rust and Swift, name lookup only finds names
declared earlier. - Classes are incomplete until the end of their definition. Unlike in C++, any
attempt to observe a property of an incomplete class that is not known until
the class is complete renders the program invalid. - When an
implneeds to be resolved, only thoseimpldeclarations that
appear earlier are considered. However, if a laterimpldeclaration would
change the result of any earlierimpllookup, the program is invalid. - Declarations in an API file that are insufficient for use by clients that
import the library require a definition in the API file.- Where possible, we should make declarations sufficient, so a definition
is not required.
- Where possible, we should make declarations sufficient, so a definition
Exceptions
Because a class is not complete until its definition has been fully parsed,
applying this rule would make it impossible to define most member functions
within the class definition. In order to still provide the convenience of
defining class member functions inline, such member function bodies are deferred
and processed as if they appeared immediately after the end of the outermost
enclosing class, like in C++.
Alternatives considered
- Allow information to be used before it is provided
globally,
within a file,
or
within a top-level declaration. - Do not allow inline method bodies to use members before they are declared
- Do not allow separate declaration and definition
File: docs/project/principles/library_apis_only.md
Principle: All APIs are library APIs
Table of contents
Background
Every major modern programming language comes with a standard library, which
consists of APIs that are not part of the core language, but instead are written
in the language (although their implementations may not be). However, different
languages draw the boundary between language and library in different places.
For example, Go's map type is built into the core language, whereas the C++
equivalent, std::unordered_map, is part of the standard library. In Swift,
even fundamental types like integers and pointers are part of the standard
library; there are no truly "built in" types.
These decisions can have important consequences for the design of the language.
For example, many important features of C++, such as move semantics, variadics,
and coroutines, were motivated largely by their anticipated uses in a small set
of standard library types. In a language with a different design philosophy,
those types could have been built into the core language. This would probably
have substantially simplified the language, and made those types available
faster. However, that would have come at the cost of less flexibility for users
outside the common case.
Principle
In Carbon, every public function is declared in some Carbon API file, and every
public interface, impl, and first-class type is defined in some Carbon API
file. In some cases, the bodies of public functions will not be defined as
Carbon code, or will be defined as hybrid Carbon code using intrinsics that
aren't available to ordinary Carbon code. However, we will try to minimize those
situations.
Thus, even "built-in" APIs can be used like user-defined APIs, by importing the
appropriate library and using qualified names from that library, relying on the
ordinary semantic rules for Carbon APIs.
Applications of this principle
We expect Carbon to have a special "prelude" library that is implicitly imported
by all Carbon source files, and there might be a special name lookup rule to
allow the names in the prelude to be used unqualified. However, in accordance
with this principle, they will remain available to ordinary qualified name
lookup as well.
According to the resolutions of
#543 and
#750, Carbon will
have a substantial number of type keywords, such as i32, f64, and bool.
However, these keywords will all be aliases for ordinary type names, such asCarbon.Int(32), Carbon.Float(64), and Carbon.Bool. Furthermore, all
arithmetic and logical operators will be overloadable, so that those types can
be defined as class types. The member function bodies for these types will be
probably not be implemented in Carbon, but this principle applies only to
function declarations, not function definitions.
Similarly, a pointer type such as Foo* will be an alias for some library class
type, for example Carbon.Ptr(Foo). As a result, Carbon will support
overloading pointer operations like -> and unary *.
All Carbon operations that use function-style syntax, such as sizeof() anddecltype() in C++, will be standard library functions. As above, in some cases
we may choose to alias those functions with keywords, and the function bodies
may not be defined in Carbon.
Exceptions
This principle applies to types only if they are first-class, meaning that
they can be the types of run-time variables, function parameters, and return
values. Carbon's type system will probably also include some types whose usage
is more restricted, and this principle will not apply to them. Most importantly,
function types might not be first-class types, in which case they need not be
library types.
Some types (such as tuples, structs, and certain integer types) will have
built-in literal syntaxes for creating values of those types. Furthermore, in
some cases (such as tuples and structs) the type's literal syntax will also be
usable as a pattern syntax. The logic for performing those operations is
arguably part of those types' public API, but will not be part of those types'
class definitions.
Alternatives considered
File: docs/project/principles/low_context_sensitivity.md
Principle: Low context-sensitivity
Table of contents
Principle
Carbon should favor designs and mechanisms that are not sensitive to context.
Instead, we should favor constructs that are not ambiguous so that they don't
need context for disambiguation. This is in service to the goal that
Carbon code is easy to read, understand, and write.
In particular, this is about prioritizing reading and understanding over
writing. We should be willing to trade off conciseness, which still benefits
reading as well as writing, for a sufficiently impactful reduction in the amount
of context needed to read and understand code.
Context can be expensive in different ways, for example:
- It can be large: it might require looking through a lot of lines of code
to find all of the relevant contextual information. - It can be distant: the further away from the current declaration or
definition, the more expensive it is to find contextual information. This
can scale from a separate definition in the same file, to a separate file,
or even to a separate package. - It can be unpredictable: it might require careful searching of large
bodies of code to locate the contextual information if its location cannot
be predicted. - It can be subtle: the contextual clues might be easily missed or mistaken.
Code that isn't context sensitive is easier to copy or move between contexts,
like files or functions. It is code that needs fewer changes when it is
refactored, in support of
software evolution.
In general, we should start with more restrictive constructs that limit
ambiguity and see if we can make them work. If we find those restrictions are
burdensome, we will then have more information to inform the next step. Ideally
we would address those use cases with simple tools that solve multiple problems.
The goal is to make a bunch of orthogonal mechanisms, each of which are easily
understood and act in unsurprising ways.
If that next step is to loosen restrictions, that is generally easier to do
while maintaining compatibility with existing code than adding new restrictions.
Mitigations of context-sensitive costs
There are several ways that the potential costs of context-sensitive code can be
mitigated. These techniques can and should be leveraged to help minimize and
mitigate the contextual costs of Carbon features, and in some cases may provide
a path to a feature that would otherwise be prohibitively costly.
Visual aids
A direct way to reduce contextual costs is through lexical and syntactic
structures that form visual aids. These can both reinforce what the context is
and aid the reader in the expensive aspect of navigating the context. For
example, representing contexts with indentation, or IDE highlighting of matching
parentheses and braces. These visual hints make it easier for developers to
notice contextual elements.
Contextual _validity_ rather than _meaning_
When the context only affects the validity of code, but not its meaning, the
costs are significantly reduced. In that case, understanding the meaning or
behavior of the code doesn't require context, and a developer can easily rely on
the compiler to check the validity. A simple example of this is contextually
valid syntax, which is relatively common and inexpensive. However, reusing the
same syntax with different contexts with different meanings shifts the
contextual information from simple validity to impacting the meaning of code.
Reduced cost of mistakes
Another mitigation for the costs of context-sensitive code is when the cost of a
mistake due to the context is low. Some simple examples:
- Context-sensitivity in comments is less expensive in general than in code.
- In places where the general meaning is clear, developers can safely and
reliably work with that general understanding, and the context only provides
a minor refinement.
Compiler-checked context
Another way the costs of mistakes can be reduced is when the compiler can
reliably detect them. This is the fundamental idea behind statically
type-checked languages: the compiler enforcement reduces the contextual cost of
knowing what the types are. How early and effectively the compiler can detect
the mistakes also plays a role in reducing this cost, which is part of the value
proposition for
definition-checked
generics.
An example of this situation in Rust is that the same syntax is used for a move
and a copy of the value in a variable. Those cases are distinguished by whether
the type implements a specific trait, which may not be readily ascertained. The
compiler verifies that the code never uses a variable that is no longer valid
due to having been moved from, which is expected to catch the problems that
could arise from this difference. Otherwise the semantic difference between a
move and a copy is considered in Rust to be low-enough stakes for there to be no
need to signal that difference in the code.
However, the reasoning that makes this example a good design on balance for Rust
doesn't necessarily apply to Carbon. The compiler is checking to prevent
errors, but it can't reliably check for unpredictable performance. Given
Carbon's priorities, that might make this level of contextual information still
too expensive.
More background on this area of Rust specifically is presented in
their blog post on language ergonomics.
Applications of the principle
There are many parts of Carbon that could potentially be analyzed through this
lens, and we can't enumerate them all here. This section focuses on several
examples to help illustrate how the principle is likely to be relevant to
Carbon. They focus on either cases that showcase the principle in effect or
cases which make challenging tradeoffs of the costs in the principle.
Imports and namespaces
There are several parts of the way
imports and
namespaces are designed
in Carbon that reflect applications of this principle:
Adding an import or reordering imports should never change behavior of
existing code. This means the reader doesn't have to look through all the
imports to understand how code behaves. This is also important for tooling,
which should not have to worry about unwanted side effects when adding or
sorting imports.Carbon doesn't provide an analogy to C++'s
using namespace
or a
"wildcard imports" mechanisms
that merge the names from one namespace into another. Either would introduce
ambiguity in where a name is coming from, making the code more
context-sensitive.Carbon doesn't support large blocks of code
inside a namespace declaration,
where the reader would have to search for the beginning of the block to see
what namespace applies.
Name shadowing
We should limit how names can be reused with shadowing rules, so the meaning of
a name doesn't change in surprising ways between scopes. Further, if you find a
matching declaration you don't have to keep searching to see if there is another
that hides the one you found. This both expands the context you have to
consider, and is an opportunity to make a mistake identifying the correct
context, potentially leading to misunderstanding of the code.
Flow-sensitive typing
This principle is an argument against
flow-sensitive typing,
where the type of a name can change depending on control flow. For example,
Midori used this for optional types.
If we were to support this in Carbon, you could unwrap an optional value by
testing it against None.
var x: Optional(Int) = ...;
if (x != None) {
// x has type Int.
PrintInt(x);
}
// x is back to type Optional(Int).This can be taken farther, this example has x taking on three different types:
var x: Optional(Optional(Int)) = ...;
if (x != None) {
// x has type Optional(Int).
if (x != None) {
// x has type Int.
PrintInt(x);
}
// x has type Optional(Int).
}
// x has type Optional(Optional(Int)).The concern here is that the context is very subtle. The type of x is affected
by otherwise ordinary-looking if statements and closing braces (}).
While we might not want to completely eliminate the possibility of
flow-sensitive typing in Carbon, it would have to overcome a large hurdle. We
would only want a flow-sensitive feature if it delivered sufficiently large
usability, consistency, or expressivity gains.
Coherence of names and generics
Carbon packages are
designed to ensure all declared names belong to exactly one package and the
compiler can enforce Carbon's equivalent
one-definition rule (ODR).
This avoids an issue in C++ where the ODR is not reliably checked by the
compiler, which can leave the correctness of programs dependent on both distant
and subtle contextual information.
Similarly, Carbon generics should have
coherence, like Rust,
where types have a single implementation of an interface. And this should be
enforced by the compiler, using rules like
Rust's orphan rules.
Performance
Since
Carbon's number one goal is performance,
it is important that the performance characteristics of code be predictable and
readily determined by readers. This argues that those characteristics should not
depend on expensive context. For example, Carbon should not provide adynamic_cast facility with the same capabilities of C++'s where distant
aspects of the inheritance structure can cause surprising performance
differences. Similarly, Carbon should try to ensure normal looking method calls
and data member access don't have the surprising performance costs caused by
virtual inheritance in C++.
More generally, Carbon should avoid features with hidden costs, particularly
when they scale based on subtle aspects of the context where those features are
used.
File: docs/project/principles/namespace_cleanliness.md
Principle: Namespace cleanliness
Table of contents
Background
Names and entities in a program can come from multiple sources -- from a local
declaration, from an import, from the standard library, or from the prelude.
Names can be imported from Carbon code or imported or derived from code written
in another language such as C++ or an interface description language such as
that of Protobuf, MIDL, or CORBA. Names can be selected for use in a program
that language designers later decide they want to use as keywords. And in order
to use a library, it is sometimes necessary to redeclare the same name that that
library chose.
This puts a lot of pressure on the language to support a free choice of naming
for entities. Different languages make different choices in this space:
- Many languages have a set of keywords that are not usable as identifiers,
with no workaround. If this set collides with a name needed by user code,
the user is left to solve this problem, often by rewriting the identifier in
some way (klassorclass_), which sometimes conflicts with the general
naming convention used by the code. And conversely, suboptimal choices are
made for new language keywords to avoid causing problems for existing code. - C and C++ reserve a family of identifiers, such as those beginning with an
underscore and a capital letter. However, it's not clear which audiences the
reserved identifiers are for, and this leads to collisions between standard
library vendors and compiler authors, as well as between implementation
extensions and language extensions.- MSVC provides a
__identifier(keyword)extension that allows using a
keyword as an identifier. This extension is also implemented by Clang in-fms-extensionsmode. - GCC provides an
__asm__(symbol)extension that allows a specific
symbol to be assigned to an object or function, which provides ABI
compatibility but not source compatibility with code that uses a keyword
as a symbol name. This extension is also implemented by Clang.
- MSVC provides a
- Python reserves some identifiers but still allows them to be freely
overwritten (such asbool) and reserves some identifiers but rejects
assignment to them (such asTrue). - Rust provides a raw identifier syntax to allow most identifiers with
reserved meaning to be used by a program, but
not all:self,Self,super,extern, andcratecannot be used as raw
identifiers. Rust also predeclares a large number of library names in every
file, but allows them to be shadowed by user declarations with the same
name. - Swift provides a raw identifier syntax using backticks:
`class`, and
is
considering
extending this to allow arbitrary non-word-shaped character sequences
between the`s.
Carbon provides
raw identifier syntax,
for example r#for, to allow using keywords as identifiers. Carbon also intends
to have strict shadowing rules that may make predeclared identifiers that are
not keywords difficult or impossible to redeclare and use in inner scopes.
Principle
In Carbon, the language does not encroach on the developer's namespace. There
are no predeclared or reserved identifiers. In cases where the language gives
special meaning to a word or to word-shaped syntax such as i32, that special
meaning can always be undone with raw identifier syntax, r#.
Conversely, when adding language keywords, we will not select an inferior
keyword merely to avoid the risk of breaking existing programs. We will still
take into account how often it is desirable to use the word as an identifier,
including in domain-specific contexts, because that is a factor in whether it
would make a good keyword, and will manage the rollout of new keywords to make
it straightforward to migrate existing uses to r# or a different name.
Applications of this principle
- Words like
finalandbasethat only have special meaning in a few
contexts, and could otherwise be made available as identifiers, are keywords
in Carbon.{.base = ...}and{.r#base = ...}specify different member
names. - Words like
selfthat are declared by the developer but nonetheless have
special language-recognized meaning are keywords in Carbon.(self)
introduces a self parameter;(r#self: Self)introduces a parameter with
the nameselfas a raw identifier. - Words like
Selfthat are implicitly declared by the language in some
contexts are keywords, even though we could treat them as user-declared
identifiers that are merely implicitly declared in some cases. - Words like
i32that are treated as type literals rather than keywords can
be used as identifiers with raw identifier syntaxr#i32. - There are no predeclared identifiers imported from the prelude. If an entity
is important enough to be available by default, we should add a keyword, and
allow the name of the entity to be used for other purposes withr#. - The predeclared package name
Coreis a keyword. A package namedr#Core
is an unrelated package, andCore.fooalways refers to members of the
predeclaredCorepackage.
Exceptions
For now, we reserve the package names Main and Cpp. These names aren't
predeclared in any scope, and the name Main is not even usable from within
source files to refer to the main package. However, there is currently no way to
avoid collisions between the package name Cpp and a top-level entity namedCpp if they are both used in the same source file.
Alternatives considered
File: docs/project/principles/one_way.md
Principle: Prefer providing only one way to do a given thing
Table of contents
Background
It's common in programming languages to provide multiple, similar ways of doing
the same thing. Sometimes this reflects the legacy of a language, and
difficulties in evolving in ways that would require changes to
developer-authored code, thereby retaining backwards compatibility. Other times
it reflects a desire to provide both verbose and concise versions of the same
syntax. We are concerned with both forms.
We also are cautious about creating alternatives that may give rise to a
paradox of choice,
wherein options are similar enough that developers actively spend time analyzing
trade-offs, and the time spent that way outweighs the potential benefits of a
correct choice.
Where multiple, similar implementation options exist, it can sometimes give rise
to style guidelines to indicate a preferential choice; sometimes because one
option is objectively better, but sometimes because making a choice is better
than not making one. Even with a style guide, developers may diverge in style by
accident or intent, choosing different coding patterns simply because either
option works. It can also become an issue as developers move between an
organization that they need to learn a new style guide, and relearn habits.
A couple examples of this in other languages are:
- In Perl,
"There is more than one way to do it." - In Python,
"There should be one -- and preferably only one -- obvious way to do it."
Principle
In Carbon, we will prefer providing only one way to do a given thing. That is,
given a syntax scenario where multiple design options are available, we will
tend to provide one option rather than providing several and letting users
choose. This echoes Python's principle.
Minimizing choices serves several goals:
- Language tools should
be easier to write and maintain with the lower language complexity implied
by less duplication of functionality. - Software and language evolution
processes should find it easier to both consider existing syntax and avoid
creation of new syntax conflicts. - Understandability of code
should be promoted if developers have less syntax they need to understand.
This can be expected to improve code quality and productivity so long as the
resulting code structures aren't overly complicated.
By minimizing the overlap of language features, we hope to make work easier for
both Carbon's maintainers and developers.
Applications of this principle
We can observe the application of this principle by comparing several language
features to C++. There, improving understandability is frequently the primary
motivation:
- Where C++ allows logical operators to be written with either symbols (for
example,&&) or text (for example,and), Carbon will only support one
form (in this case, text). - Where C++ allows hexadecimal numeric literals to be either lowercase
(0xaa) or uppercase (0xAA), and withxoptionally uppercase as well,
Carbon will only allow the0xAAcasing. - Where C++ provides both
structandclasswith the only difference is
access control defaults, Carbon will only provide one (class, albeit with
default public visibility diverging from C++).
However, sometimes language tools are the primary motivation. For example, where
C++ allows braces to be omitted for single-statement control flow blocks, Carbon
will require braces. This offers a
syntax simplification that should allow for better error detection.
Caveats
Specialized syntax
Sometimes overlap will occur because a specialized syntax offers particular
benefits, typically as a matter of convenience for either a common use-case or a
particularly complex and important use-case. Some examples of why and where this
occurs are:
- For performance, it
may at times be necessary to provide a specialized syntax that better
supports optimization than a generic syntax. - For
understandability of code,
there may be times that a particular use-case is common enough that
simplifying its syntax provides substantial benefit.- For example,
for (var x: auto in list)could typically be written with
as awhileloop, but range-based for loops are considered to improve
understandability. However, C++'sfor (;;)syntax is sufficiently
close towhilethat we expect to usewhileto address the
corresponding use-cases.
- For example,
- For
migration and interoperability,
it may be pragmatic to provide both an ideal way of doing things for new
Carbon code, and a separate approach that is more C++-compatible for
migration.- For example, consider generics and templates: generics are considered to
be the preferred form for new code, but templates are considered a
necessity for migration of C++ code. This is not an evolution situation
because we do not anticipate ever removing templates.
- For example, consider generics and templates: generics are considered to
Non-obvious alternatives
Echoing Python, there may be non-obvious alternative ways of doing a given
thing, such as using while (condition) { DoSomething(); break; } in place ofif (condition) { DoSomething(); }. As a more complex example, lambdas could be
implemented using other code constructs; this would require significantly more
code and hinder understandability.
This kind of overlap may exist, but will hopefully be considered sufficiently
non-idiomatic that examples won't be common in code. If a choice would not
likely be based mainly on coding styles, it's likely sufficiently distinct that
this principle won't apply.
In evolution
For evolution, it will
often be necessary to temporarily provide an "old" and "new" way of doing things
simultaneously.
For example, if renaming a language feature, it may be appropriate to provide
the same functionality under two identifiers. However, one should be marked as
deprecated and eventually removed. We should be cautious of adding new,
overlapping features without a plan to remove the corresponding legacy version.
Alternatives considered
File: docs/project/principles/README.md
Principles
Some language goals will have widely-applicable, high-impact, and
sometimes non-obvious corollaries. We collect concrete language design
principles in this directory as a way to document and clarify these. Principles
clarify, but do not supersede, goals and priorities. Principles should be used
as a tool in making decisions, and to clarify to contributors how decisions are
expected to be made.
A key difference between a principle and the design of a language feature is
that a principle should inform multiple designs, whereas a feature's design is
typically more focused on achieving a specific goal or set of goals. The
principle can help achieve consistency across those multiple designs.
Note that these principles seek to establish both the approaches the project
wants to pursue, as well as those we want to exclude.
- Errors are values
- Information accumulation
- Low context-sensitivity
- Prefer providing only one way to do a given thing
- One static open extension mechanism
- Success criteria
- File concatenation
File: docs/project/principles/safety_strategy.md
Safety strategy
Table of contents
- Background
- Philosophy
- Principles
- Details
- Caveats
- Alternatives considered
Background
Carbon's goal is to provide
practical safety and testing mechanisms.
What "safety" means in Carbon
Safety is protection from software bugs, whether the protection is required by
the language or merely an implementation option. Application-specific logic
errors can be prevented by testing, but can lead to security vulnerabilities in
production. Safety categories will be referred to using names based on the type
of
security vulnerability
they protect against.
A key subset of safety categories Carbon should address are:
Memory safety protects
against invalid memory accesses. Carbon uses
two main subcategories
for memory safety:Spatial memory safety protects against accessing an address that's out
of bounds for the source. This includes array boundaries, as well as
dereferencing invalid pointers such as uninitialized pointers,NULLin
C++, or manufactured pointer addresses.Temporal memory safety protects against accessing an address that has
been deallocated. This includes use-after-free for heap and
use-after-return for stack addresses.
Type safety protects
against accessing valid memory with an incorrect type, also known as "type
confusion".Data race safety
protects against racing memory access: when a thread accesses (read or
write) a memory location concurrently with a different writing thread and
without synchronizing.
Safety guarantees versus hardening
In providing safety, the underlying goal is to prevent attacks from turning a
logic error into a security vulnerability. The three ways of doing this can
be thought of in terms of how they prevent attacks:
Safety guarantees prevent bugs. They offer a strong requirement that a
particular security vulnerability cannot exist. Compile-time safety checks
are always a safety guarantee, but safety guarantees may also be done at
runtime. For example:At compile-time, range-based for loops offer a spatial safety guarantee
that out-of-bounds issues cannot exist in the absence of concurrent
modification of the sequence.At runtime, garbage collected languages offer a temporal safety
guarantee because objects cannot be freed while they're still
accessible.
Error detection checks for common logic errors at runtime. For example:
An array lookup function might offer spatial memory error detection by
verifying that the passed index is in-bounds.A program can implement reference counting to detect a temporal memory
error by checking whether any references remain when memory is freed.
Safety hardening mitigates bugs, typically by minimizing the feasibility
of an attack. For example:Control Flow Integrity (CFI)
monitors for behavior which can subvert the program's control flow. In
Clang, it is
optimized for use in release builds. Typically CFI analysis will only
detect a subset of attacks because it can't track each possible code
path separately. It should still reduce the feasibility of both spatial
memory, temporal memory, and type attacks.Memory tagging
makes each attempt at an invalid read or write operation have a high
probability of trapping, while still not detecting the underlying bug in
every case. Realistic attacks require many such operations, so memory
tagging may stop attacks in some environments. Alternatively, the trap
might be asynchronous, leaving only a tiny window of time prior to the
attack being detected and program terminated. These are probabilistic
hardening and reduces the feasibility of both spatial and temporal
memory attacks.
Under both error detection and safety hardening, even if a safety is protected,
the underlying bugs will still exist and will need to be fixed. For example,
program termination could be used for a denial-of-service attack.
Philosophy
Carbon's
practical safety and testing mechanisms
will emphasize guaranteed safety where feasible without creating barriers to
Carbon's other goals, particularly performance and
interoperability. This limits Carbon's options for guaranteed safety, and as a
result there will be more reliance upon error detection and safety hardening.
The language's design should incentivize safe programming, although it will not
be required.
When writing code, Carbon developers should expect to receive safety without
needing to add safety annotations. Carbon will have optional safety annotations
for purposes such as optimizing safety checks or providing information that
improves coverage of safety checks.
Carbon will favor compile-time safety checks because catching issues early will
make applications more reliable. Runtime checks, either error detection or
safety hardening, will be enabled where safety cannot be proven at compile-time.
There will be three high-level use cases or directions that Carbon addresses
through different build modes that prioritize safety checks differently:
- A debug oriented build mode that prioritizes detecting bugs and
reporting errors helpfully. - A performance oriented build mode that skips any dynamic
safety checks to reduce overhead. - A hardened oriented build mode that prioritizes ensuring
sufficient safety to prevent security vulnerabilities, although it may not
allow detecting all of the bugs.
These high level build modes may be tuned, either to select specific nuanced
approach for achieving the high level goal, or to configure orthogonal
constraints such as whether to prioritize binary size or execution speed.
However, there is a strong desire to avoid requiring more fundamental build
modes to achieve the necessary coverage of detecting bugs and shipping software.
These build modes are also not expected to be interchangeable or compatible with
each other within a single executable -- they must be a global selection.
Although expensive safety checks could be provided through additional build
modes, Carbon will favor safety checks that can be combined into these three
build modes rather than adding more.
Over time, safety should evolve
using a hybrid compile-time and runtime safety approach to eventually provide a
similar level of safety to a language that puts more emphasis on guaranteed
safety, such as
Rust. However,
while Carbon may encourage developers to modify code in support of more
efficient safety checks, it will remain important to improve the safety of code
for developers who cannot invest into safety-specific code modifications.
Principles
Safety must be
easy to ramp-up with,
even if it means new developers don't receive the full safety that Carbon
can offer.Developers should benefit from Carbon's safety without needing to learn
and apply Carbon-specific design patterns. Some safety should be enabled
by default, without safety-specific work, although some safety will
require work to opt in. Developers concerned with performance should
only need to disable safety in rare edge-cases.Where there is a choice between safety approaches, the safe option
should be incentivized by making it equally easy or easier to use. If
there is a default, it should be the safe option. It should be
identifiable when the unsafe option is used. Incentives will prioritize,
in order:- Guaranteed safety.
- Error detection.
- Safety hardening.
- Unsafe and unmitigated code.
Language design choices should favor more efficient implementations of
safety checks. They should also allow favor automation of testing and
fuzzing.
Safety in Carbon must work with
interoperable or migrated C++ code,
so that C++ developers can readily take advantage of Carbon's improvements.Safety mechanisms will ideally be designed to apply to automatically
migrated C++ code. Providing immediate safety improvements to Carbon
adopters will help motivate adoption.In the other direction, safety mechanisms must not force manual
rewriting of C++ code in order to migrate, either by creating design
incompatibilities or performance degradations. Automated migration of
C++ code to Carbon must work for most developers, even if it forces
Carbon's safety design to take a different approach.Carbon's safety should degrade gracefully when Carbon code calls C++
code, although this may require use of the Carbon toolchain to compile
the C++ code. Applications should be expected to use interoperability.
Although some safety features will be Carbon-specific, safety should not
stop at the language boundary.
The rules for determining whether code will pass compile-time safety
checking should be articulable, documented, and easy to understand.- Compile-time safety checks should not change significantly across
different build modes. The purpose of the build modes is to determine
code generation.
- Compile-time safety checks should not change significantly across
Each build mode will prioritize performance and safety differently:
The debug build mode will produce development-focused binaries
that prioritize fast iteration on code with safety checks that assist in
identification and debugging of errors.The performance build mode will produce release-focused
binaries that prioritize performance over safety.The hardened build mode will produce release-focused
binaries that prioritize safety that is resistant to attacks at the cost
of performance.
Safety checks should try to be identical across build modes.
- There will be differences, typically due to performance overhead and
detection rate trade-offs of safety check algorithms.
- There will be differences, typically due to performance overhead and
The number of build modes will be limited, and should be expected to remain
at the named three.Most developers will use two build modes in their work: debug for
development, and either performance or hardened for releases.It's important to focus on checks that are cheap enough to run as part
of normal development. Users are not expected to want to run additional
development build modes for additional sanitizers.Limiting the number of build modes simplifies support for both Carbon
maintainers, who can focus on a more limited set of configurations, and
Carbon developers, who can easily choose which is better for their
use-case.
Each distinct safety-related build mode (debug, performance, and hardened)
cannot be combined with others in the same binary.- Cross-binary interfaces will exist in Carbon, and will need to be used
by developers interested in combining libraries built under different
build modes.
- Cross-binary interfaces will exist in Carbon, and will need to be used
Although runtime safety checks should prevent logic errors from turning into
security vulnerabilities, the underlying logic errors will still be bugs.
For example, some safety checks would result in application termination;
this prevents execution of unexpected code and still needs to be fixed.Developers need a strong testing methodology to engineer correct software.
Carbon will encourage testing and then leverage it with the checking build
modes to find and fix bugs and vulnerabilities.
Details
Incremental work when safety requires work
Carbon is prioritizing usability of the language, particularly minimizing
retraining of C++ developers and easing migration of C++ codebases, over the
kind of provable safety that some other languages pursue, particularly Rust.
A key motivation of Carbon is to move C++ developers to a better, safer
language. However, if Carbon requires manually rewriting or redesigning C++ code
in order to maintain performance, it creates additional pressure on C++
developers to learn and spend time on safety. Safety will often not be the top
priority for developers; as a result, Carbon must be thoughtful about how and
when it forces developers to think about safety.
Relying on multiple build modes to provide safety should fit into normal
development workflows. Carbon can also have features to enable additional
safety, so long as developers can start using Carbon in their applications
without learning new paradigms.
Where possible, safety checks shouldn't require work on the part of Carbon
developers. A safety check that requires no code edits or can be handled by
automated migration may be opt-out, as there is negligible cost to developers.
One which requires local code changes should be opt-in because costs will scale
with codebase size. Safety check approaches which would require substantial
redesign by developers will be disfavored based on adoption cost, even if the
alternative is a less-comprehensive approach.
Using build modes to manage safety checks
Carbon will likely start in a state where most safety checks are done at
runtime. However, runtime detection of safety violations remains expensive. In
order to make as many safety checks as possible available to developers, Carbon
will adopt a strategy based on three build modes that target key use-cases.
Debug
The debug build mode targets developers who are iterating on code and running
tests. It will emphasize detection and debuggability, especially for safety
issues.
It needs to perform well enough to be run frequently by developers, but will
make performance sacrifices to catch more safety issues. This mode should have
runtime checks for the most common safety issues, but it can make trade-offs
that improve performance in exchange for less frequent, but still reliable,
detection. Developers should do most of their testing in this build mode.
The debug build mode will place a premium on the debuggability of safety
violations. Where safety checks rely on hardening instead of guaranteed safety,
violations should be detected with a high probability per single occurrence of
the bug. Detected bugs will be accompanied by a detailed diagnostic report to
ease classification and root cause identification.
Performance
The performance build mode targets the typical application that wants high
performance from Carbon code, where performance considers processing time,
memory, and disk space. Trade-offs will be made that maximize the performance.
Only safety techniques that don't measurably impact application hot path
performance will be enabled by default. This is a very high bar, but is crucial
for meeting Carbon's performance goals, as well as allowing migration of
existing C++ systems which may not have been designed with Carbon's safety
semantics in mind.
Hardened
The hardened build mode targets applications where developers want strong safety
against attacks in exchange for worse performance. It will work to prevent
attacks in ways that
attackers cannot work around,
even if it means using techniques that create significant performance costs.
Managing bugs without compile-time safety
Carbon's reliance on runtime checks will allow developers to manage their
security risk. Developers will still need to reliably find and fix the
inevitable bugs, including both safety violations and regular business logic
bugs. The cornerstone of managing bugs will be strong testing methodologies,
with built-in support from Carbon.
Strong testing is more than good test coverage. It means a combination of:
Ensuring unsafe or risky operations and interfaces can easily be recognized
by developers.Using static analysis tools to detect common bugs, and ensuring they're
integrated into build and code review workflows. These could be viewed as
static testing of code.Writing good test coverage, including unit, integration, and system tests.
Implementing coverage-directed fuzz testing to discover bugs outside of
manually authored test coverage, especially for interfaces handling
untrusted data. Fuzz testing is a robust way to catch bugs when APIs may be
used in ways developers don't consider.Running continuous integration, including automatic and continuous running
of these tests. The checked development build mode should be validated, as
well as any additional build modes necessary to cover different forms of
behavior checking.Easing automated testing and fuzzing through language features. For example,
if the language encourages value types and pure functions of some sort, they
can be automatically fuzzed.
These practices are necessary for reliable, large-scale software engineering.
Maintaining correctness of business logic over time requires continuous and
thorough testing. Without it, such software systems cannot be changed and
evolved over time reliably. Carbon will reuse these practices in conjunction
with checking build modes to mitigate the limitations of Carbon's safety
guarantees without imposing overhead on production systems.
When a developer chooses to use Carbon, adhering to this kind of testing
methodology is essential for maintaining safety. As a consequence, Carbon's
ecosystem, including the language, tools, and libraries, will need to directly
work to remove barriers and encourage the development of these methodologies.
The reliance on testing may make Carbon a poor choice in some environments; in
environments where such testing rigor is infeasible, a language with a greater
degree of static checking may be better suited.
Caveats
Probabilistic techniques likely cannot stop attacks
It's expected that probabilistic techniques that can be applied at the language
level are attackable through a variety of techniques:
- The attacker might be able to attack repeatedly until it gets through.
- The attacker may be able to determine when the attack would be detected and
only run the attack when it would not be. - The attacker might be able control the test condition to make detection much
less likely or avoid detection completely. For example, if detection is
based on the last 4 bits of a memory address, an attacker may be able to
generate memory allocations, viewing the address and only attacking when
there's a collision.
Hardware vulnerabilities may make these attacks easier than they might otherwise
appear. Future hardware vulnerabilities are difficult to predict.
Note this statement focuses on what can be applied to the language level. Using
a secure hash algorithm, such as SHA256, may be used to offer probabilistic
defense in other situations. However, the overhead of a secure hash algorithm's
calculation is significant in the context of most things that Carbon may do at
the language level.
Combining these issues, although it may seem like a probabilistic safety check
could be proven to reliably detect attackers, it's likely infeasible to do so.
For the various build modes, this means:
- The debug build mode will not typically be accessible to attackers, so where
a probabilistic technique provides a better developer experience, it will be
preferred. - The performance build mode will often avoid safety checks in order to reach
peak performance. As a consequence, even the weak protection of a
probabilistic safety check may be used in order to provide some
protection. - The hardened build mode will prefer non-probabilistic techniques that
cannot be attacked.
Alternatives considered
Guaranteed memory safety programming models
Multiple approaches that would offer guaranteed memory safety have been
considered, mainly based on other languages which offer related approaches.
Carbon will likely rely more on error detection and hardening because of what
the models would mean for Carbon's performance and C++ migration language goals.
Guaranteed compile-time memory safety using borrow checking
Rust offers a good example of an approach for compile-time safety based on
borrow checking, which provides guaranteed safety. For code which can't
implement borrow checking, runtime safety using reference counting is available
and provides reliable error detection. This approach still allows forunsafe blocks, as
well as types that offer runtime safety while wrapping unsafe interfaces.
Carbon could use a similar approach for guaranteed safety by default.
Advantages:
- Guaranteed safety, including against data races, is provided for the
binaries.- The emphasis on compile-time safety limits the scope of the runtime
memory safety costs. - With Rust, there is early evidence that there's a significant impact in
reducing bugs generally.
- The emphasis on compile-time safety limits the scope of the runtime
- Imitating Rust's techniques would allow building on the huge work of the
Rust community, reducing the risks of implementing similar in Carbon. - Careful use of narrow
unsafeescape hatches can be effectively
encapsulated behind otherwise safe APIs.
Disadvantages:
- Rust's approach to compile-time safety requires use of
design patterns and idioms
that are substantially different from C++.- Conversion of C++ code to Rust results in either rewrites of code, or
use of runtime safety checks that impair performance. - Requires fully modeling lifetime and exclusivity in the type system.
- Data structures must be redesigned to avoid sharing mutable state.
- Increases complexity of node and pointer based data structures, such as
linked lists.
- Conversion of C++ code to Rust results in either rewrites of code, or
- Imitating Rust's techniques may prove insufficient for achieving Carbon's
compiler performance goals.
Rust compilation performance suggests its borrow checking performance is
slow, although it's difficult to determine how significant this is or
whether it could be improved.- The Rust compiler
is slow,
although
much has been done to improve it. - Details of type checking, particularly requiring parsing of function
bodies to type check signatures, as well as wide use of
monomorphization
are likely significant contributors to Rust compilation performance. - LLVM codegen is also a significant cost for Rust compilation
performance. - With
Fuchsia as
an example, in December 2020, borrow checking and type checking combined
account for around 10% of Rust compile CPU time, or 25% of end-to-end
compile time. The current cost of borrow checking is obscured both
because of the combination with type checking, and because Fuchsia
disables some compiler parallelization due to build system
incompatibility.
- The Rust compiler
- The complexity of using Rust's compile-time safety may incentivize
unnecessary runtime checking of safety properties. For example, usingRefCellorRcto avoid changing
designs to fit compile-time safety models. - Some of the most essential safety tools that ease the ergonomic burden of
the Rust-style lifetime model (Rc) introduce semantic differences that
cannot then be eliminated in a context where performance is the dominant
priority.
It's possible to modify the Rust model several ways in order to reduce the
burden on C++ developers:
- Don't offer safety guarantees for data races, eliminating
RefCell.- This would likely not avoid the need for
RcorArc, and wouldn't
substantially reduce the complexity.
- This would likely not avoid the need for
- Require manual destruction of
Rc, allowing safety checks to be disabled in
the performance build mode to eliminate overhead.- This still requires redesigning C++ code to take advantage of
Rc. - The possibility of incorrect manual destruction means that the safety
issue is being turned into a bug, which means it is hardening and no
longer a safety guarantee. - Carbon can provide equivalent hardening through techniques such as
MarkUs,
which does not require redesigning C++ code.
- This still requires redesigning C++ code to take advantage of
Overall, Carbon is making a compromise around safety in order to give a path for
C++ to evolve. C++ developers must be comfortable migrating their codebases, and
able to do so in a largely automated manner. In order to achieve automated
migration, Carbon cannot require fundamental redesigns of migrated C++ code.
While a migration tool could in theory mark all migrated code as unsafe,
Carbon should use a safety strategy that degrades gracefully and offers
improvements for C++ code, whether migrated or not.
That does not mean Carbon will never adopt guaranteed safety by default, only
that performance and migration of C++ code takes priority, and any design will
need to be considered in the context of other goals. It should still be possible
to adopt guaranteed safety later, although it will require identifying a
migration path.
Guaranteed run-time memory safety using reference counting
Reference counting is a
common memory safety model, with Swift as a popular example.
Advantages:
- Simple model for safety, particularly as compared with Rust.
- Safe for all of the most common and important classes of memory safety bugs.
Disadvantages:
- Safety based on reference counting introduces significant performance costs,
and tools for controlling these costs are difficult.- Safety based on garbage collection has less direct performance overhead,
but has a greater unpredictability of performance.
- Safety based on garbage collection has less direct performance overhead,
- Significant design differences versus C++ still result, as the distinction
between value types and "class types" becomes extremely important.- Class types are held by a reference counted pointer and are thus
lifetime safe.
- Class types are held by a reference counted pointer and are thus
In order to mitigate the performance overhead, Swift does have a proposal to add
an option for unique ownership, although the specifics are not designed yet. The
unique ownership approach is expected to require unowned and unsafe access, so
it would not considered to improve the safety trade-offs.
Swift was designated by Apple as the replacement for Objective-C. The safety
versus performance trade-offs that it makes fit Apple's priorities. Carbon's
performance goals should lead to different trade-off decisions with a higher
priority on peak performance, which effectively rules out broad use of reference
counting.
Guaranteed run-time memory safety using garbage collection
Garbage collection
is a common memory safety model, with Java as a popular example.
Advantages:
- This approach is among the most robust and well studied models, with decades
of practical usage and analysis for security properties. - Extremely suitable for efficient implementation on top of a virtual machine,
such as the JVM.
Disadvantages:
- Extremely high complexity to fully understand the implications of complex
cases like data races. - Performance overhead is significant in terms of what Carbon would like to
consider.- Garbage collection remains a difficult performance problem, even for the
JVM and its extensive optimizations. - The complexity of the implementation makes it difficult to predict
performance; for example, Java applications experience latency spikes
when garbage collection runs.
- Garbage collection remains a difficult performance problem, even for the
Java is a good choice for many applications, but Carbon is working to focus on a
set of performance priorities that would be difficult to achieve with a garbage
collector.
Build mode names
The build mode concepts are difficult to name. Other names that were evaluated,
and are ultimately similar, are:
"Debug" is a common term for the intended use of this build mode. Also,
tooling including Visual Studio frequently uses the debug term for
describing similar.- "Development" was also considered, but this term is less specific and
would be better for describing all non-release builds together. For
example, a "fast build" mode might be added that disables safety checks
to improve iteration time, like might be controlled by way of C++'sNDEBUGoption.
- "Development" was also considered, but this term is less specific and
"Performance" aligns with the phrasing of the language performance goal.
"Optimized" implies that other modes would not be fully optimized, but
hardened should be optimized."Fast" would suggest that speed is the only aspect of performance being
optimizing for, but "performance" also optimizes for memory usage and
binary size.
"Hardened" is the choice for succinctly describing the additional safety
measures that will be taken, and is a well-known term in the safety space.
It could be incorrectly inferred that "performance" has no hardening, but
the preference is to clearly indicate the priority of the "hardened" build
mode."Safe" implies something closer to guaranteed safety. However, safety
bugs should be expected to result in program termination, which can
still be used in other attacks, such as Denial-of-Service."Mitigated" is an overloaded term, and it may not be succinctly clear
that it's about security mitigations.
Some terms which were considered and don't fit well into the above groups
are:- "Release" is avoided because both "performance" and "hardened" could be
considered to be "release" build modes.
- "Release" is avoided because both "performance" and "hardened" could be
The names "performance" and "hardened" may lead to misinterpretations, with some
developers who should use "hardened" using "performance" because they are
worried about giving up too much performance, and the other way around. The
terms try to balance the utility of well-known terminology with the succinctness
of a short phrase for build modes, and that limits the expressivity. Some
confusion is expected, and documentation as well as real-world experience (for
example, a developer who cares about latency benchmarking both builds) should be
expected to help mitigate mix-ups.
Performance versus safety in the hardened build mode
The performance cost of safety techniques are expected to be non-linear with
respect to detection rates. For example, a particular vulnerability such as heap
use-after-free may be detectable with 99% accuracy at 20% performance cost, but
100% accuracy at 50% performance cost. At present, build modes should be
expected to evaluate such a scenario as:
- The debug build mode would choose the 99% accurate approach.
- Detecting safety issues is valuable for debugging.
- The probabilistic detection rate won't meaningfully affect accuracy of
tests. - The lower performance cost improves developer velocity.
- The performance build mode would decline detection.
- Safety checks with a measurable performance cost should be declined.
- The hardened build mode would choose the 100% accurate approach.
- Safety must be non-probabilistic in order to reliably prevent attacks.
- Significant performance hits are acceptable.
- This means the hardened build mode may be slower than the debug build
mode.
In order to achieve better performance, the hardened build mode could make
trade-offs closer to the debug build mode. Rather than relying on
non-probabilistic techniques, it could instead offer a probability-based chance
of detecting a given attack.
Advantages:
- Probabilistic safety should come at lower performance cost (including CPU,
memory, and disk space).- This will sometimes be significant, and as a result of multiple checks,
could result in the hardened build mode being 50% slower than the
performance build mode instead of being 200% slower.
- This will sometimes be significant, and as a result of multiple checks,
Disadvantages:
- Probabilistic techniques likely cannot stop attacks.
- Attackers may be able to repeat attacks until they succeed.
- The variables upon which the probability is based, such as memory
addresses, may be manipulable by the attacker. As a consequence, a
determined attacker may be able to manipulate probabilities and not even
be detected.
Although performance is
Carbon's top goal, the hardened
build mode exists to satisfy developers and environments that value safety more
than performance. The hardened build mode will rely on non-probabilistic safety
at significant performance cost because other approaches will be insufficient to
guard against determined attackers.
Add more build modes
More build modes could be added to this principle, or the principle could
encourage the idea that specific designs may add more.
To explain why three build modes:
The concept of debug and release (sometimes called opt) are common. For
example, in
Visual Studio.
In Carbon, this could be considered to translate to the "debug" and
"performance" build modes by default.The hardened build mode is added in order to emphasize security. Although
hardened could be implemented as a set of options passed to the standard
release build mode, the preference is to focus on it as an important
feature.
An example of why another build mode may be needed is
ThreadSanitizer, which is
noted as having 5-15x slowdown and 5-10x memory overhead. This is infeasible for
normal use, but could be useful for some users in a separate build mode. A
trade-off that's possible for Carbon is instead using an approach similar to
KCSAN which offers relatively
inexpensive but lower-probability race detection.
Although options to these build modes may be supported to customize deployments,
the preference is to focus on a small set and make them behave well. For
example, if a separate build mode is added for ThreadSanitizer, it should be
considered a temporary solution until it can be merged into the debug build
mode.
Advantages:
- Grants more flexibility for using build modes as a solution to problems.
- With safety checks, this would allow providing safety checks that are
high overhead but also high detection rate as separate build modes. - With other systems, there could be non-safety performance versus
behavior trade-offs.
- With safety checks, this would allow providing safety checks that are
Disadvantages:
- Having standard modes simplifies validation of interactions between various
safety checks.- Safety is the only reason that's been considered for adding build modes.
- As more build modes are added, the chance of developers being confused and
choosing the wrong build mode for their application increases.
Any long-term additions to the set of build modes will need to update this
principle, raising the visibility and requiring more consideration of such an
addition. If build modes are added for non-safety-related reasons, this may lead
to moving build modes out of the safety strategy.
Experiment: This can be considered an experiment. Carbon may eventually add
more than the initial three build modes, although the reluctance to add more is
likely to remain.