mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-03 07:15:49 +01:00
As part of using the evolution process with the toolchain, alternatives should be in proposals. This proposal migrates existing alternatives here. Assisted-by: Google Antigravity with Gemini 3 Flash
670 lines
29 KiB
Markdown
670 lines
29 KiB
Markdown
# Check
|
||
|
||
<!--
|
||
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
|
||
-->
|
||
|
||
<!-- toc -->
|
||
|
||
## Table of contents
|
||
|
||
- [Overview](#overview)
|
||
- [Subtopics](#subtopics)
|
||
- [Postorder processing](#postorder-processing)
|
||
- [Key IR concepts](#key-ir-concepts)
|
||
- [Instruction operands](#instruction-operands)
|
||
- [Parameters and arguments](#parameters-and-arguments)
|
||
- [SemIR textual format](#semir-textual-format)
|
||
- [Raw form](#raw-form)
|
||
- [Formatted IR](#formatted-ir)
|
||
- [Instructions](#instructions)
|
||
- [Top-level entities](#top-level-entities)
|
||
- [Core loop](#core-loop)
|
||
- [Node stack](#node-stack)
|
||
- [Delayed evaluation (not yet implemented)](#delayed-evaluation-not-yet-implemented)
|
||
- [Templates (not yet implemented)](#templates-not-yet-implemented)
|
||
- [Rewrites](#rewrites)
|
||
- [Types](#types)
|
||
- [Type printing (not yet implemented)](#type-printing-not-yet-implemented)
|
||
- [Expression categories](#expression-categories)
|
||
- [ExprCategory::NotExpression](#exprcategorynotexpression)
|
||
- [ExprCategory::Value](#exprcategoryvalue)
|
||
- [ExprCategory::DurableReference and ExprCategory::EphemeralReference](#exprcategorydurablereference-and-exprcategoryephemeralreference)
|
||
- [ExprCategory::Initializing](#exprcategoryinitializing)
|
||
- [ExprCategory::Mixed](#exprcategorymixed)
|
||
- [Value bindings](#value-bindings)
|
||
- [Handling Parse::Tree errors (not yet implemented)](#handling-parsetree-errors-not-yet-implemented)
|
||
- [Alternatives considered](#alternatives-considered)
|
||
|
||
<!-- tocstop -->
|
||
|
||
## Overview
|
||
|
||
Check takes the parse tree and generates a semantic intermediate representation,
|
||
or SemIR. This will look closer to a series of instructions, in preparation for
|
||
transformation to LLVM IR. Semantic analysis and type checking occurs during the
|
||
production of SemIR. It also does any validation that requires context.
|
||
|
||
## Subtopics
|
||
|
||
Some particular topics have their own documentation:
|
||
|
||
- [Associated constants](associated_constant.md)
|
||
- [Pattern matching](pattern_matching.md)
|
||
|
||
## Postorder processing
|
||
|
||
The checking step is oriented on postorder processing on the `Parse::Tree` to
|
||
iterate through the `Parse::NodeImpl` vectorized storage once, in order, as much
|
||
as possible. This is primarily for performance, but also relies on the
|
||
[information accumulation principle](/docs/project/principles/information_accumulation.md):
|
||
that is, when that principle applies, we should be able to generate IR
|
||
immediately because we can rely on the principle that when a line is processed,
|
||
the information necessary to semantically check that line is already available.
|
||
|
||
Indirectly, what this really means is that we should be able to go from a
|
||
Parse::Tree (which cannot be used for name lookups) to a SemIR with name lookups
|
||
completed in a single pass. The SemIR should not need to be re-processed to add
|
||
more information outside of templates. By doing this, we avoid an additional
|
||
processing pass with associated storage needs.
|
||
|
||
This single-pass approach also means that the checking step does not make use of
|
||
the tree structure of the `Parse::Tree`. In cases where the actions performed
|
||
for a parse tree node depend on the context in which that node appears, a node
|
||
that is visited earlier in the postorder traversal, such as a bracketing node,
|
||
needs to establish the necessary context. In this respect, the sequence of
|
||
`Parse::Node`s can be thought of as a byte code input that the check step
|
||
interprets to build the `SemIR`.
|
||
|
||
## Key IR concepts
|
||
|
||
A `SemIR::Inst` is the basic building block that represents a simple
|
||
instruction, such as an operator or declaring a literal. For each kind of
|
||
instruction, a struct for that specific kind of instruction is provided in the
|
||
`SemIR` namespace. For example, `SemIR::Assign` represents an assignment
|
||
instruction, and `SemIR::PointerType` represents a pointer type instruction.
|
||
|
||
Each instruction class has up to four public data members describing the
|
||
instruction, as described in
|
||
[sem_ir/typed_insts.h](/toolchain/sem_ir/typed_insts.h) (also see
|
||
[adding features for Check](/toolchain/docs/adding_features.md#check)):
|
||
|
||
- An `InstKind kind;` member if the instruction has a `Kinds` constant making
|
||
it a shorthand for multiple individual instructions.
|
||
|
||
- A `SemIR::TypeId type_id;` member that describes the type of the instruction
|
||
is present on all instructions that produce a value. This includes namespace
|
||
instructions, which are modeled as producing a value of "namespace" type,
|
||
even though they can't be used as a first-class value in Carbon expressions.
|
||
|
||
- Up to two additional [kind-specific members](#instruction-operands). For
|
||
example `SemIR::Assign` has members `InstId lhs_id` and `InstId rhs_id`. And
|
||
`SemIR::ClassElementAccess` has an `ElementIndex` into the fields of the
|
||
class to define which field it is accessing.
|
||
|
||
Instructions are stored as type-erased `SemIR::Inst` objects, which store the
|
||
instruction kind and the (up to) four fields described above. This balances the
|
||
size of `SemIR::Inst` against the overhead of indirection.
|
||
|
||
All instructions have with them a `SemIR::LocId` that tracks their source
|
||
location, and is stored separately on the `InstStore`. A `LocId` that refers
|
||
indirectly to the location of an instruction can be created by calling the
|
||
`LocId` constructor: `SemIR::LocId(inst_id)`. You can use
|
||
`insts().GetCanonicalLocId(loc_id)` to flatten the location to one that refers
|
||
directly to either a parse node or an imported location, but this is typically
|
||
necessary only when decomposing the location or when marking it with the
|
||
implicit or token-only flags.
|
||
|
||
A `SemIR::InstBlockId`, used to index into `SemIR::InstBlockStore`, can
|
||
represent a code block. However, it can also be created when a series of
|
||
instructions needs to be closely associated, such as a parameter list.
|
||
|
||
A number of instruction types in
|
||
[sem_ir/typed_insts.h](/toolchain/sem_ir/typed_insts.h) are builtin
|
||
instructions, such as `SemIR::TypeType` which represents the unconstrained facet
|
||
type `type`. Builtins have stable ids in the `SemIR::InstStore` across `SemIR`
|
||
instances.
|
||
|
||
### Instruction operands
|
||
|
||
The kind-specific members on a typed instruction struct can be of any type
|
||
listed in the `SemIR::IdKind` enumeration defined in
|
||
[sem_ir/id_kind.h](/toolchain/sem_ir/id_kind.h), typically derived from
|
||
`IdBase`. The most commonly used kinds refer to other instructions:
|
||
|
||
- `SemIR::InstId`: Refers to a specific instance of an instruction. For
|
||
example, this should be used if the operand may have side-effects or a
|
||
meaningful location.
|
||
- `SemIR::ConstantId`: An abstract reference to a known constant value. This
|
||
should be used instead of `InstId` if you care only about the identity of
|
||
the value and not how it was formed.
|
||
- `SemIR::TypeId`: An abstract reference to a known constant value of type
|
||
`type`. This should be used instead of `ConstantId` if you know that the
|
||
type of the constant value is always `type` (or the value is the "error"
|
||
constant, whose type is also the "error" constant).
|
||
|
||
Other ID types are used for more specialized purposes. Refer to the class
|
||
documentation for the ID type for more details.
|
||
|
||
### Parameters and arguments
|
||
|
||
Parameters and arguments will be stored as two blocks (referred to by
|
||
`SemIR::InstBlockId`) each. The first will contain the full IR, while the second
|
||
will contain references to the last instruction for each parameter or argument.
|
||
The references block will have a size equal to the number of parameters or
|
||
arguments, allowing for quick size comparisons and indexed access.
|
||
|
||
## SemIR textual format
|
||
|
||
There are two textual ways to view `SemIR`.
|
||
|
||
### Raw form
|
||
|
||
The raw form of SemIR shows the details of the representation, such as numeric
|
||
instruction and block IDs. The representation is intended to very closely match
|
||
the `SemIR::File` and `SemIR::Inst` representations. This can be useful when
|
||
debugging low-level issues with the `SemIR` representation.
|
||
|
||
The driver will print this when passed `--dump-raw-sem-ir`.
|
||
|
||
### Formatted IR
|
||
|
||
In addition to the raw form, there is a higher-level formatted IR that aims to
|
||
be human readable. This is used in most `check` tests to validate the output,
|
||
and also expected to be used regularly by toolchain developers to inspect the
|
||
result of checking the parse tree.
|
||
|
||
The driver will print this when passed `--dump-sem-ir`.
|
||
|
||
Unlike the raw form, certain representational choices in the `SemIR` data may
|
||
not be visible in this form. However, it is intended to be possible to parse the
|
||
`SemIR` output and form an equivalent – but not necessarily identical – `SemIR`
|
||
representation, although no such parser currently exists.
|
||
|
||
As an example, given the program:
|
||
|
||
```carbon
|
||
fn Cond() -> bool;
|
||
fn Run() -> i32 { return if Cond() then 1 else 2; }
|
||
```
|
||
|
||
The formatted IR is currently:
|
||
|
||
```
|
||
constants {
|
||
%.1: i32 = int_literal 1 [template]
|
||
%.2: i32 = int_literal 2 [template]
|
||
}
|
||
|
||
file {
|
||
package: <namespace> = namespace [template] {
|
||
.Cond = %Cond
|
||
.Run = %Run
|
||
}
|
||
%Cond: <function> = fn_decl @Cond [template] {
|
||
%return.var.loc1: ref bool = var <return slot>
|
||
}
|
||
%Run: <function> = fn_decl @Run [template] {
|
||
%return.var.loc2: ref i32 = var <return slot>
|
||
}
|
||
}
|
||
|
||
fn @Cond() -> bool;
|
||
|
||
fn @Run() -> i32 {
|
||
!entry:
|
||
%Cond.ref: <function> = name_ref Cond, file.%Cond [template = file.%Cond]
|
||
%.loc2_33.1: init bool = call %Cond.ref()
|
||
%.loc2_26.1: bool = value_of_initializer %.loc2_33.1
|
||
%.loc2_33.2: bool = converted %.loc2_33.1, %.loc2_26.1
|
||
if %.loc2_33.2 br !if.expr.then else br !if.expr.else
|
||
|
||
!if.expr.then:
|
||
%.loc2_41: i32 = int_literal 1 [template = constants.%.1]
|
||
br !if.expr.result(%.loc2_41)
|
||
|
||
!if.expr.else:
|
||
%.loc2_48: i32 = int_literal 2 [template = constants.%.2]
|
||
br !if.expr.result(%.loc2_48)
|
||
|
||
!if.expr.result:
|
||
%.loc2_26.2: i32 = block_arg !if.expr.result
|
||
return %.loc2_26.2
|
||
}
|
||
```
|
||
|
||
There are three kinds of names in formatted IR, which are distinguished by their
|
||
leading sigils:
|
||
|
||
- `%name` denotes a value produced by an instruction. These names are
|
||
introduced by a line of the form `%name: <category> <type> = <instruction>`,
|
||
and are scoped to the enclosing top-level entity. `<category>` describes the
|
||
[expression category](#expression-categories), which is `init` for an
|
||
initializing expression, `ref` for a reference expression, or omitted for a
|
||
value expression. Typically, values can only be referenced by instructions
|
||
that their introduction
|
||
[dominates](<https://en.wikipedia.org/wiki/Dominator_(graph_theory)>), but
|
||
some kinds of instruction might have other rules. Names in the `file` block
|
||
can be referenced as `file.%<name>`.
|
||
|
||
- `!name` denotes a label, and `!name:` appears as a prefix of each
|
||
`InstBlock` in a `Function`. These names are scoped to their enclosing
|
||
function, and can be referenced anywhere in that function, but not outside.
|
||
|
||
- `@name` denotes a top-level entity, such as a function, class, or interface.
|
||
The SemIR view of these entities is flattened, so member functions are
|
||
treated as top-level entities.
|
||
|
||
Names in formatted IR are all invented by the formatter, and generally are of
|
||
the form `<base_name>[.loc<line>[_<col>[.<counter>]]]` where `<line>` and
|
||
`<col>` describe the location of the instruction, and `<counter>` is used as a
|
||
disambiguator if multiple instructions appear at the same location. Trailing
|
||
name components are only included if they are necessary to disambiguate the
|
||
name. `<base_name>` is a guessed good name for the instruction, often derived
|
||
from source-level identifiers, and is empty if no guess was made.
|
||
|
||
#### Instructions
|
||
|
||
There is usually one line in a `InstBlock` for each `Inst`. You can find the
|
||
documentation for the different kinds of instructions in
|
||
[toolchain/sem_ir/typed_insts.h](/toolchain/sem_ir/typed_insts.h). For example,
|
||
given a formatted SemIR line like:
|
||
|
||
```
|
||
%N: i32 = assoc_const_decl N [template]
|
||
```
|
||
|
||
you would look for a `struct` definition that uses `"assoc_const_decl"` as its
|
||
`ir_name`. In this case, this is the `AssociatedConstantDecl` instruction:
|
||
|
||
```cpp
|
||
// An associated constant declaration in an interface, such as `let T:! type;`.
|
||
struct AssociatedConstantDecl {
|
||
static constexpr auto Kind =
|
||
InstKind::AssociatedConstantDecl.Define<Parse::NodeId>(
|
||
{.ir_name = "assoc_const_decl", .is_lowered = false});
|
||
|
||
TypeId type_id;
|
||
NameId name_id;
|
||
};
|
||
```
|
||
|
||
Since this instruction produces a value, it has a `TypeId type_id` field, which
|
||
corresponds to the type written between the `:` and the `=`. In the example
|
||
above, that type is `i32`. The other arguments to the instruction are written
|
||
after the `ir_name` -- in this example the `name_id` is `N`. From this we find
|
||
that the instruction corresponds to an associated constant declaration in an
|
||
interface like `let N:! i32;`.
|
||
|
||
In fact, the notation after the `:` records not just the instruction's type, but
|
||
also some information about its category, and the storage argument if it's an
|
||
initializer:
|
||
|
||
- `%N: i32 = ...`: a value expression of type `i32`.
|
||
- `%N: init i32 = ...`: an initializing expression of type `i32` with no
|
||
storage argument.
|
||
- `%N: init i32 to %s = ...`: an initializing expression of type `i32` that
|
||
uses `%s` as its backing storage if needed.
|
||
- `%N: ref i32 = ...`: a reference of type `i32`.
|
||
|
||
Instructions producing a constant value, like `assoc_const_decl` above, are
|
||
followed by their phase, either `[symbolic]` or `[template]`, and then `=` the
|
||
value if it is the value of a different instruction.
|
||
|
||
Instructions that do not produce a value, such as the `br` and `return`
|
||
instructions above, omit the leading `%name: ... =` prefix, as they cannot be
|
||
named by other instructions. These instructions do not have a `TypeId type_id`
|
||
field, like the `AdaptDecl` instruction:
|
||
|
||
```cpp
|
||
// An adapted type declaration in a class, of the form `adapt T;`.
|
||
struct AdaptDecl {
|
||
static constexpr auto Kind = InstKind::AdaptDecl.Define<Parse::AdaptDeclId>(
|
||
{.ir_name = "adapt_decl", .is_lowered = false});
|
||
|
||
// No type_id; this is not a value.
|
||
TypeId adapted_type_id;
|
||
};
|
||
```
|
||
|
||
An `adapt SomeClass;` declaration would have the corresponding SemIR formatted
|
||
as:
|
||
|
||
```
|
||
adapt_decl %SomeClass
|
||
```
|
||
|
||
Some instructions have special argument handling. For example, some invalid
|
||
arguments will be omitted. Or an `InstBlockId` argument will be rendered inline,
|
||
commonly enclosed in braces `{`...`}` or parens `(`...`)`. In other cases, the
|
||
formatter will combine instructions together to make the IR more readable:
|
||
|
||
- A terminator sequence in a block, comprising a sequence of `BranchIf`
|
||
instructions followed by a `Branch` or `BranchWithArg` instruction, is
|
||
collapsed into a single
|
||
`if %cond br !label1 else if ... else br !labelN(%arg)` line.
|
||
- A struct type, formed by a sequence of `StructTypeField` instructions
|
||
followed by a `StructType` instruction, is collapsed into a single
|
||
`struct_type{.field1: %value1, ..., .fieldN: %valueN}` line.
|
||
|
||
As noted above, initializer storage arguments are formatted with `... to %arg`
|
||
in the type position. To parallel that, the destination argument of a `return`
|
||
instruction is also formatted with `to`. For example:
|
||
|
||
```
|
||
return %N to %return.param
|
||
```
|
||
|
||
These exceptions may be found in
|
||
[toolchain/sem_ir/formatter.cpp](/toolchain/sem_ir/formatter.cpp).
|
||
|
||
#### Top-level entities
|
||
|
||
**Question:** Are these too in flux to document at this time?
|
||
|
||
- `constants`: TODO
|
||
- `imports`: TODO
|
||
- `file`: TODO
|
||
- entities
|
||
- TODO: may be preceded by `extern`.
|
||
- TODO: may be preceded by `generic`.
|
||
- These may have an optional `!definition:` section containing the
|
||
generic's `definition_block_id`.
|
||
- `fn`: TODO; followed by `= "`...`"` for builtins
|
||
- `class`: TODO
|
||
- `interface`: TODO
|
||
- `impl`: TODO
|
||
- `specific`: TODO
|
||
- body in braces `{`...`}` has a bunch of
|
||
``<generic parameter> => <specific value>` assignment lines
|
||
- The first lines of the body describe the declaration
|
||
- If there is a valid definition, there are additional definition
|
||
assignments after a `!definition:` line.
|
||
|
||
## Core loop
|
||
|
||
The core loop is `Check::CheckParseTree`. This loops through the `Parse::Tree`
|
||
and calls a `Handle`... function corresponding to the `NodeKind` of each node.
|
||
Communication between these functions for different nodes working together is
|
||
through the `Context` object defined in
|
||
[check/context.h](/toolchain/check/context.h), which stores things in a
|
||
collection of stacks. The common pattern is that the children of a node are
|
||
processed first. They produce information that is then consumed when processing
|
||
the parent node.
|
||
|
||
One example of this pattern is expressions. Each subexpression outputs SemIR
|
||
instructions to compute the value of that subexpression to the current
|
||
instruction block, added to the top of the `InstBlockStack` stored in the
|
||
`Context` object. It leaves an instruction id on the top of the
|
||
[node stack](#node-stack) pointing to the instruction that produces the value of
|
||
that subexpression. Those are consumed by parent operations, like an
|
||
[RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation) calculator. For
|
||
example, the expression `1 * 2 + 3` corresponds to this parse tree:
|
||
|
||
```yaml
|
||
{kind: 'IntegerLiteral', text: '1'},
|
||
{kind: 'IntegerLiteral', text: '2'},
|
||
{kind: 'InfixOperator', text: '*', subtree_size: 3},
|
||
{kind: 'IntegerLiteral', text: '3'},
|
||
{kind: 'InfixOperator', text: '+', subtree_size: 5},
|
||
```
|
||
|
||
This parse tree is processed by one call to a `Handle` function per node:
|
||
|
||
- The first node is an integer literal, so the core loop calls
|
||
`HandleIntegerLiteral`.
|
||
|
||
- It calls `context::AddInstAndPush` to output a `SemIR::IntegerLiteral`
|
||
instruction to the current instruction block, and pushes the parse node
|
||
along with the instruction id to the [node stack](#node-stack).
|
||
|
||
- The second node is also an integer literal, which outputs a second
|
||
instruction and pushes another entry onto the node stack.
|
||
|
||
- `HandleInfixOperator` pops the two entries off of the node stack, outputs
|
||
any conversion instructions that are needed, and uses
|
||
`context::AddInstAndPush` to create and push the instruction id representing
|
||
the output of a multiplication instruction. That multiplication instruction
|
||
takes the instruction ids it popped off the stack at the beginning as
|
||
arguments.
|
||
|
||
- Another integer literal instruction is created for `3` and pushed onto the
|
||
stack.
|
||
|
||
- `HandleInfixOperator` is called again. It pops the two instruction ids off
|
||
the stack to use as the arguments to the multiplication instruction it
|
||
creates and pushes.
|
||
|
||
In this way, the handle functions coordinate producing their output using the
|
||
instruction block stack and node block stack from the context.
|
||
|
||
A similar pattern uses bracketing nodes to support parent nodes that can have a
|
||
variable number of children. For example, a `return` statement can produce parse
|
||
trees following a few different patterns:
|
||
|
||
- `return;`
|
||
|
||
```yaml
|
||
{kind: 'ReturnStatementStart', text: 'return'},
|
||
{kind: 'ReturnStatement', text: ';', subtree_size: 2},
|
||
```
|
||
|
||
- `return x;`
|
||
|
||
```yaml
|
||
{kind: 'ReturnStatementStart', text: 'return'},
|
||
{kind: 'NameExpr', text: 'x'},
|
||
{kind: 'ReturnStatement', text: ';', subtree_size: 3},
|
||
```
|
||
|
||
- `return var;`
|
||
|
||
```yaml
|
||
{kind: 'ReturnStatementStart', text: 'return'},
|
||
{kind: 'ReturnVarModifier', text: 'var'},
|
||
{kind: 'ReturnStatement', text: ';', subtree_size: 3},
|
||
```
|
||
|
||
In all three cases, the introducer node `ReturnStatementStart` pushes an entry
|
||
on the [node stack](#node-stack) with just the parse node and no id, called a
|
||
_solo parse node_. The handler for the parent `ReturnStatement` node can pop and
|
||
process entries from the node stack until it finds that solo parse node from
|
||
`ReturnStatementStart` that indicates it is done.
|
||
|
||
Another pattern that arises is state is set up by an introducer node, updated by
|
||
its siblings, and then consumed by the bracketing parent node. TODO: example
|
||
|
||
### Node stack
|
||
|
||
The node stack, defined in [check/node_stack.h](/toolchain/check/node_stack.h),
|
||
stores pairs of a `Parse::Node` and an id. The type of the id is determined by
|
||
the `NodeKind` of the parse node. It is the default, general-purpose stack used
|
||
by `Handle`... functions in the check stage. Using a single stack is beneficial
|
||
since it improves locality of reference and reduces allocations. However,
|
||
additional stacks are used to ensure we never need to search through the stack
|
||
to find data -- we always want to be operating on the top of the stack (or a
|
||
fixed offset).
|
||
|
||
The node stack contains any state pushed by siblings of the current
|
||
`Parse::Node` at the top, and state pushed by siblings of ancestors below. The
|
||
boundaries between what is a sibling of the current `Parse::Node` versus what is
|
||
a sibling of an ancestor are not explicitly determined. Instead, the handler for
|
||
the parent node knows how many nodes it must pop from the stack based either on
|
||
knowing the fixed number of children for that node kind or popping nodes until
|
||
it reaches a bracketing node. The arity or bracketing node kind for each parent
|
||
node is documented in [parse/node_kind.def](/toolchain/parse/node_kind.def).
|
||
|
||
When each `Parse::Node` is evaluated, the SemIR for it is typically immediately
|
||
generated as `SemIR::Inst`s. To help generate the IR to an appropriate context,
|
||
scopes have separate `SemIR::InstBlock`s.
|
||
|
||
### Delayed evaluation (not yet implemented)
|
||
|
||
Sometimes, nodes will need to have delayed evaluation; for example, an inline
|
||
definition of a class member function needs to be evaluated after the class is
|
||
fully declared. The `SemIR::Inst`s cannot be immediately generated because they
|
||
may include name references to the class. We're likely to store a reference to
|
||
the relevant `Parse::Node` for each definition for re-evaluation after the class
|
||
scope completes. This means that nodes in a definition would be traversed twice,
|
||
once while determining that they're inline and without full checking or IR
|
||
generation, then again with full checking and IR generation.
|
||
|
||
### Templates (not yet implemented)
|
||
|
||
Templates need to have partial semantic checking when declared, but can't be
|
||
fully implemented before they're instantiated against a specific type.
|
||
|
||
We are likely to generate a partial IR for templates, allowing for checking with
|
||
the incomplete information in the IR. Instantiation will likely use that IR and
|
||
fill in the missing information, but it could also reevaluate the original
|
||
`Parse::Node`s with the known template state.
|
||
|
||
### Rewrites
|
||
|
||
Carbon relies on rewrites of code, such as rewriting the destination of an
|
||
initializer to a specific target object once that object is known.
|
||
|
||
We have two ways to achieve this. One is to track the IR location of a
|
||
placeholder instruction and, if it needs updating, replace it with a "rewrite"
|
||
`SemIR::Inst` that points to a new `SemIR::InstBlock` containing the required IR
|
||
and specifying which value is the result of that rewrite. This is expressed in
|
||
SemIR as a `splice_block` instruction. Another is to track the list of
|
||
instructions to be created separately from the node block stack, and merge those
|
||
instructions into the current block once we have decided on their contents.
|
||
|
||
## Types
|
||
|
||
Type expressions are treated like any other expression, and are modeled as
|
||
`SemIR::Inst`s. The types computed by type expressions are deduplicated,
|
||
resulting in a canonical `SemIR::TypeId` for each distinct type.
|
||
|
||
### Type printing (not yet implemented)
|
||
|
||
The `TypeId` preserves only the identity of the type, not its spelling, and so
|
||
printing it will produce a fully-resolved type name, which isn't a great user
|
||
experience as it doesn't reflect how the type was written in the source code.
|
||
|
||
Instead, when printing a type name for use in a diagnostic, we will start with
|
||
one of two `InstId`s:
|
||
|
||
- A `InstId` for a type expression that describes the way the type was
|
||
computed.
|
||
- A `InstId` for an expression that has the given type.
|
||
|
||
In the former case, the type is pretty-printed by walking the type expression
|
||
and printing it. In the latter case, the type of the expression is reconstructed
|
||
based on the form of the expression: for example, to print the type of `&x`, we
|
||
print the type of `x` and append a `*`, being careful to take potential
|
||
precedence issues into account.
|
||
|
||
TODO: This requires being able to print the type of, for example,
|
||
`x.foo[0].bar`, by printing only the desired portion of the type of `x`, and
|
||
similarly may require handling the case where the type of an expression involves
|
||
generic parameters whose arguments are specified by that expression. In effect,
|
||
the type computation performed when checking an operation is duplicated into the
|
||
type printing logic, but is simpler because errors don't need to be detected.
|
||
|
||
This approach means we don't need to preserve a fully-sugared type for each
|
||
expression instruction. Instead, we compute that type when we need to print it.
|
||
|
||
## Expression categories
|
||
|
||
Each `SemIR::Inst` that has an associated type also has an expression category,
|
||
which describes how it produces a value of that type. These
|
||
`SemIR::ExprCategory` values correspond to the Carbon expression categories
|
||
defined in proposal
|
||
[#2006](https://github.com/carbon-language/carbon-lang/pull/2006):
|
||
|
||
### ExprCategory::NotExpression
|
||
|
||
This instruction is not an expression instruction, and doesn't have an
|
||
expression category. This is used for namespaces, control flow instructions, and
|
||
other constructs that represent some non-expression-level semantics.
|
||
|
||
### ExprCategory::Value
|
||
|
||
This instruction produces a value using the type's value representation.
|
||
Lowering the instruction will produce an LLVM value using that value
|
||
representation.
|
||
|
||
### ExprCategory::DurableReference and ExprCategory::EphemeralReference
|
||
|
||
This instruction produces a reference to an object. Lowering will produce a
|
||
pointer to an object representation.
|
||
|
||
### ExprCategory::Initializing
|
||
|
||
This instruction represents the initialization of an object. Depending on the
|
||
initializing representation for the type, the initializing expression
|
||
instruction will do one of the following:
|
||
|
||
- For an in-place initializing representation, the instruction will store a
|
||
value to the target of the initialization.
|
||
|
||
- For a by-copy initializing representation, the instruction will produce an
|
||
object representation by value that can be stored into the target. This is
|
||
currently only used in cases where the object representation and the value
|
||
representation are the same.
|
||
|
||
- For a type with no initializing representation, such as an empty struct or
|
||
tuple, it does neither of the above things.
|
||
|
||
Regardless of the initializing representation, an initializing expression should
|
||
be consumed by another instruction that finishes the initialization. For a
|
||
by-copy initialization, this final instruction represents the store into the
|
||
target, whereas in the other cases it is only used to track in SemIR how the
|
||
initialization was used. When an in-place initializer uses a by-copy initializer
|
||
as a subexpression, an `initialize_from` instruction is inserted to perform this
|
||
final store.
|
||
|
||
### ExprCategory::Mixed
|
||
|
||
This instruction represents a language construct that doesn't have a single
|
||
expression category. This is used for struct and tuple literals, where the
|
||
elements of the literal can have different expression categories. Instructions
|
||
with a mixed expression category are treated as a special case in conversion,
|
||
which recurses into the elements of those instructions before performing
|
||
conversions.
|
||
|
||
### Value bindings
|
||
|
||
A value binding represents a conversion from a reference expression to the value
|
||
stored in that expression. There are three important cases here:
|
||
|
||
- For types with a by-copy value representation, such as `i32`, a value
|
||
binding represents a load from the address indicated by the reference
|
||
expression.
|
||
|
||
- For types with a by-pointer value representation, such as arrays and large
|
||
structs and tuples, a value binding implicitly takes the address of the
|
||
reference expression.
|
||
|
||
- For structs and tuples, the value representation is a struct or tuple of the
|
||
elements' value representations, which is not necessarily the same as a
|
||
struct or tuple of the elements' object representations. In the case where
|
||
the value representation is not a copy of, or pointer to, the object
|
||
representation, `value_binding` instructions are not used, and a
|
||
`tuple_value` or `struct_value` instruction is used to construct a value
|
||
representation instead. `value_binding` should still be used in the case
|
||
where the value and object representation are the same, but this is not yet
|
||
implemented.
|
||
|
||
## Handling Parse::Tree errors (not yet implemented)
|
||
|
||
`Parse::Tree` errors will typically indicate that checking would error for a
|
||
given context. We'll want to be careful about how this is handled, but we'll
|
||
likely want to generate diagnostics for valid child nodes, then reduce
|
||
diagnostics once invalid nodes are encountered. We should be able to reasonably
|
||
abandon generated IR of the valid children when we encounter an invalid parent,
|
||
without severe effects on surrounding checks.
|
||
|
||
For example, an invalid line of code in a function might generate some
|
||
incomplete IR in the function's `SemIR::InstBlock`, but that IR won't negatively
|
||
interfere with checking later valid lines in the same function.
|
||
|
||
## Alternatives considered
|
||
|
||
- [Using a traditional AST representation](/proposals/p6716.md#using-a-traditional-ast-representation)
|