Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SemIR fidelity when representing rewrite semantics #3833

Merged
merged 10 commits into from
May 23, 2024
231 changes: 231 additions & 0 deletions proposals/p3833.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# SemIR fidelity when representing rewrite semantics

<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->

[Pull request](https://github.com/carbon-language/carbon-lang/pull/3833)

<!-- toc -->

## Table of contents

- [Abstract](#abstract)
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Work to incorporate needed simplifications directly into the design](#work-to-incorporate-needed-simplifications-directly-into-the-design)
- [Keep a full fidelity mode for any other optimizations](#keep-a-full-fidelity-mode-for-any-other-optimizations)
- [Example: overloaded operators](#example-overloaded-operators)
- [Rationale](#rationale)
- [Alternatives considered](#alternatives-considered)
- [Strictly adhere to a full-fidelity model](#strictly-adhere-to-a-full-fidelity-model)
- [Directly implement the optimized model](#directly-implement-the-optimized-model)

<!-- tocstop -->

## Abstract

The toolchain's [Semantic IR][semir] should start off modeling the full,
complex, and rich library-based and generic extension point semantics of Carbon
without eliding any layers or rewrites for compile time efficiency. We shouldn't
front-load elision or optimization when implementing the designs.
jonmeow marked this conversation as resolved.
Show resolved Hide resolved

Once we have a full-fidelity implementation, we should work to build an
efficient elision, short-circuit, or common case simplification into the design
itself sufficient to make the SemIR model efficient. Only if we cannot find a
reasonable approach for that should we diverge the SemIR model to optimize its
efficiency, and we should preserve full fidelity in an optional mode.

[semir]:
https://docs.google.com/document/d/1RRYMm42osyqhI2LyjrjockYCutQ5dOf8Abu50kTrkX0/edit?resourcekey=0-kHyqOESbOHmzZphUbtLrTw#heading=h.503m6lfcnmui

## Problem

Carbon's design heavily leverages types and APIs defined in the `Core` package
and implicitly imported with the prelude. It also defines rich extension points
and generic semantic models for the language, typically through rewrites from
the user-facing syntax into more complex syntax that operates through
`interface`s and `impl`s that provide the generic model and customization
points.

Modeling this at full fidelity in the toolchain's [Semantic IR][semir] is
expected to create an unreasonably expensive representation for common and
pervasive patterns in code. On the other hand, every divergence between the
SemIR model and the design adds both risk and cost. When we can keep the two in
sync, we get the best engineering tradeoffs, provided it is reasonably
efficient.

## Background

Several proposals have started to surface these kinds of challenge, but to a
lesser degree. Some examples:

- [Proposal #820 - Implicit conversions](https://github.com/carbon-language/carbon-lang/pull/820)
- [Proposal #845 - `as` expressions](https://github.com/carbon-language/carbon-lang/pull/845)
- [Proposal #1083 - Arithmetic expressions](https://github.com/carbon-language/carbon-lang/pull/1083)

The most significant step that motivates capturing an explicit strategy here is
[proposal #3720 - Binding operators](https://github.com/carbon-language/carbon-lang/pull/3720).

## Proposal
chandlerc marked this conversation as resolved.
Show resolved Hide resolved

Initially, the toolchain should have SemIR model the full semantics of Carbon,
with all rewrites, expressive hooks, and library references. This will serve two
purposes as we build out the toolchain's support for all of Carbon's features.

### Work to incorporate needed simplifications directly into the design

First, this will allow us to study the resulting SemIR, and understand exactly
how and where the efficiency explodes for expected coding patterns. We should
use this to try to adjust the design to build in targeted common-case
simplifications and elisions that make precise modeling of the design
sufficiently cheap in practice.

We see this pattern emerging in good ways already today in the toolchain with
implicit conversions: when calling functions, we need to allow for implicit
conversions. But we don't want to have an identity implicit conversion on all
the arguments which already are the correct type. Rather than notionally
"always" implicitly converting, we make the design itself specify that the
implicit conversion only occurs when needed and can then have the desired
minimal representation in SemIR.

Beyond simplifying the SemIR model, it also ends up addressing an important
aspect of the semantics -- we don't want to create unnecessary temporaries to
initialize rather than directly initializing. The toolchain in fact follows
similar patterns to model expression categories for similar reasons -- the
minimal semantic model is also the desired design.

We can even incorporate other kinds of design changes to allow the full fidelity
SemIR model to be efficient, such as assuring that we can build and reuse a
cached representation repeatedly and cheaply rather than requiring it to be
rebuilt on each instance of a specific pattern.

### Keep a full fidelity mode for any other optimizations

We may still ultimately end up unable to achieve the desired SemIR efficiency
even after making all the simplifications we can to the design itself. If and
when we reach this point, we will still want to have the full fidelity
implementation and be able to use it behind some option. We can use it to
understand how the design works in complex cases and to debug unexpected
behavior in the toolchain. This means starting with that implementation doesn't
result in sunk cost.

### Example: overloaded operators

> Note: this example isn't trying to capture all the nuance of the current
> design or suggest any change to the design. It is merely trying to provide an
> illustration of how we might follow the proposal in practice.

As an example to help illustrate this, how should the toolchain model overloaded
operators? This proposal suggests that _initially_, the implementation should
aim to fully model the rewrite-based dispatch through interfaces in the prelude.
That is, each use of `x + y` should turn into roughly equivalent SemIR as would
be used to model the rewritten semantics of
`x.(package#Core.AddWith(typeof(y)).Op)(y)`. This in turn would dispatch to an
exact-type implementation which would provide any implicit conversions, and
so-on. And importantly, this would occur even when we know that the types of `x`
chandlerc marked this conversation as resolved.
Show resolved Hide resolved
and `y` are exactly `i32` and we could alternatively directly hard code the
toolchain to use the desired builtin implementation.

To be precise, the expectation is that the SemIR for `x + y` should as a
consequence model all of:

- Looking up the `package#Core.AddWith` interface, using the imagined syntax
`package#<Name>` for directly looking up a specific package name (as opposed
to doing unqualified name lookup).
- Passing the type of `y` as a parameter to the interface, which is
illustrated with the imagined syntax `typeof(<expression>)` but would likely
directly use the type rather than forming an actual `typeof` expression (see
below).
- Looking up the `Op` method.
- Calling that `Op` method on `x` with `y` as its parameter just as we would
model any call to `object.(Interface.Method)(y)`.

> Note: this isn't a proposal for either the `package#<Name>` or
> `typeof(<expression>)` imagined syntaxes. If we want these syntaxes, that will
> be a separate proposal.

However, specific things that would still be reasonable without deviating from
the full semantic modeling here:

- Not representing a user-written `typeof` style expression to compute the
type, or duplicating the `y` expression.
- Caching and re-using SemIR for immutable things like successfully resolved
name lookup. Concretely, re-using a single cached lookup of
`package#Core.AddWith` would still provide the full model. And similarly,
re-using an already computed parameterization such as
`package#Core.AddWith(i32)` repeatedly, and the lookup of `Op` within that
parameterization.

The reason is that these don't change the semantics in any way -- they are
constructively the same results.

This should be the initial implementation _goal_, not a requirement for any
incremental progress on implementation. Short-circuiting or other approximations
used in incrementally building towards this goal should always be reasonable to
consider based on the tradeoffs at hand. But they should be seen as temporary,
incremental measures and where possible removed so we can evaluate the full
semantic model when the implementation is complete.

Based on the practical costs that this kind of model incurs once we have it
implemented, we should first look for ways to adjust the design itself to avoid
forming the complex semantic expansion in enough cases to make the model
scalable. And only when we've exhausted those options begin adding long-term
shortcuts in the SemIR model for common cases with a option to disable them for
testing and debugging.

## Rationale
zygoloid marked this conversation as resolved.
Show resolved Hide resolved

- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- It's easy when specifying the design of the language with rewrite
semantics in a way that accidentally has a cycle -- where the rewrite
result is still a construct that should be rewritten. Without the
proposed constraint, these accidental cycles are easily resolved in the
implementation by exiting at a convenient place. However, this proposal
will ensure that the rule for where the cycle is broken is actually
surfaced into the design, making the design more robust and complete in
describing the expected behavior.
- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem)
- Preserving a full fidelity model of the language design in SemIR helps
ensure we can build rich and powerful tools that reason about even
complex or typically "hidden" parts of Carbon's design. This has
regularly proven critical in the design of effective tooling for C++ and
other languages.
- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development)
- We can't afford for full fidelity representation of Carbon's design to
make the practical use of the toolchain have poor compile times and slow
down the development experience with Carbon. We need to have a strategy
for addressing the expected overhead of the level of customization and
layered design that we're building into Carbon.

## Alternatives considered

### Strictly adhere to a full-fidelity model

We could simply insist on never deviating from the full-fidelity model. However,
this would force us to compromise at least one of our top-level goals for
Carbon: either we would have to regress the speed and scalability of
development, or lose some of the simplicity and cohesive design.

Both of those seem worse outcomes than having some limited divergence between
SemIR and the design, and there exist reasonable mitigations such as a option to
switch between modes.

### Directly implement the optimized model

We could directly jump to an optimized model that we expect to match the common
case behavior, and add opt-in complexity when needed for non-common-cases.
However, this would both remove a useful tool in driving simplification of the
design itself that can result in a overall superior tradeoff. It would also
create risk of divergence, and the need to eventually implement mode to disable
the optimization.

In some cases, we would end up discovering a need to simplify the design for
semantic reasons, and the divergence would have resulted in wasted effort in the
toolchain.

Starting with the full fidelity model seems to more reliably converge the design
early and result in a minimal set of effective optimizations.