Builtin Functions in the Carbon Toolchain
Builtin functions are compiler-recognized primitives mapping directly from
Carbon code expressions (via standard prelude bindings) to optimized backend
execution. This document defines the complete structural workflow, C++ patterns,
constant evaluation logic, machine lowering mechanics, library bindings, and
validation strategies required to implement builtin functions in the Carbon
compiler.
Technical Flow & Lifecycle
graph TD
Src[Carbon Source Code] -->|Prelude Map| Sem[Semantic Analysis / SemIR]
Sem -->|Signature Constraint| Sig[builtin_function_kind.cpp]
Sem -->|Phase Evaluation| Eval[eval.cpp Constant Interpreter]
Sem -->|Machine Codegen| Lower[handle_call.cpp LLVM Lowering]
Eval -->|Diagnostics| Diag[diagnostics/kind.def]
Lower -->|Native Instructions| LLVM[LLVM IR Generation]
Adding a builtin function involves a 5-step integration:
- Define the Builtin Kind: Register the enum in
builtin_function_kind.def.
- Signature & Compile-Time Registry: Declare the mapping name, parameter
constraints, and compile-time evaluation residency in
builtin_function_kind.cpp.
- Compile-Time Interpreter Support: Wire constant evaluation hooks and
bounds/exception diagnostics in
eval.cpp.
- LLVM IR Lowering Support: Connect target machine generation in
handle_call.cpp.
- Prelude Library Mapping: Bind primitive interfaces to named builtins
under core/prelude/.
Detailed Step-by-Step Implementation Guide
Step 1: Kind Definition & Registration
Register your builtin function name using the X-macro in
builtin_function_kind.def:
// toolchain/sem_ir/builtin_function_kind.def
// Converts an integer type to a floating-point type.
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvertFloat)
Step 2: Signature Validation & Compile-Time Residence
Inside
builtin_function_kind.cpp:
Define Parameter Constraints: If the parameter requires novel
constraints (e.g. "must be a float type"), define a template constraint
struct checking the matching SemIR type instruction (such as FloatType
or FloatLiteralType). Use pre-established semantic helpers:
TypeParam<I, T>: Ensures different parameters resolve to identical
type structures (e.g., generic constraint matching).
AnyInt, AnyFloat, AnySizedInt, AnySizedFloat, CharCompatible,
StdInitializerList, NoReturn.
Map Literal Name & Register Constraint Signature: Declare a
BuiltinInfo constant inside namespace BuiltinFunctionInfo matching the
macro-defined name:
// toolchain/sem_ir/builtin_function_kind.cpp
constexpr BuiltinInfo IntConvertFloat = {
"int.convert_float", ValidateSignature<auto(AnyInt)->AnyFloat>};
Establish Compile-Time Residency Status: Update
BuiltinFunctionKind::IsCompTimeOnly to determine if a call requires
compile-time evaluation:
- Checked/Diagnostics Primitives: Return
true immediately. Runtime
lowering of these is illegal (e.g. IntConvertFloatChecked).
- Runtime Primitives: Return
AnyLiteralTypes(sem_ir, arg_ids, return_type_id) to enforce that
expressions involving unsized literal values (like IntLiteral or
FloatLiteral) are evaluated exclusively at compile-time (as they lack
runtime representation).
Step 3: Constant Evaluation Support
Wire the interpreter inside eval.cpp to
execute compile-time computations:
Implement Constant Evaluation Logic:
Handle the builtin case inside MakeConstantForBuiltinCall (which
processes the compile-time execution of the call).
Confirm type validation phase is Phase::Concrete to reject incomplete
bindings:
case SemIR::BuiltinFunctionKind::IntConvertFloat: {
if (phase != Phase::Concrete) {
return MakeConstantResult(context, call, phase);
}
return PerformIntToFloatConvert(context, loc_id, arg_ids[0], call.type_id,
/*require_exact=*/false);
}
Extract inputs safely from local value stores (e.g.
context.ints().Get(arg.int_id) or
context.floats().Get(arg.float_id)).
Leverage high-precision LLVM mathematical structures (llvm::APInt,
llvm::APFloat, llvm::APSInt) to handle custom bits and signedness
safely.
Diagnose Invalid Parameters or Exceptions:
Define compile-time diagnostics inside
kind.def:
// toolchain/diagnostics/kind.def
CARBON_DIAGNOSTIC_KIND(IntTooLargeForFloatType)
Emplace localized diagnostic formatting messages where they are caught
in eval.cpp:
CARBON_DIAGNOSTIC(IntTooLargeForFloatType, Error,
"integer value {0} too large for floating-point type {1}",
llvm::APSInt, SemIR::TypeId);
context.emitter().Emit(loc_id, IntTooLargeForFloatType, val, dest_type_id);
Return SemIR::ErrorInst::ConstantId to gracefully abort invalid
constant generation rather than crashing the compiler.
Fast-Path Range Limits:
- Before evaluating expensive math operations on giant exponents (e.g.
1.0e1000000), executing range limits check against dest_width + 64
(sized) or IntStore::MaxIntWidth (unsized) is mandatory to prevent
out-of-bounds calculations and compile-time memory exhaustion.
Step 4: Machine Code Generation (LLVM Lowering)
Inside handle_call.cpp:
Map to Native LLVM Instructions: For runtime-eligible builtins, map the
call inside HandleBuiltinCall to native LLVM IR builder methods:
case SemIR::BuiltinFunctionKind::IntConvertFloat: {
auto* operand = context.GetValue(arg_ids[0]);
auto* dest_type = context.GetTypeOfInst(inst_id);
bool is_signed = IsSignedInt(context, arg_ids[0]);
context.SetLocal(
inst_id, is_signed
? context.builder().CreateSIToFP(operand, dest_type)
: context.builder().CreateUIToFP(operand, dest_type));
return;
}
Assert on Compile-Time-Only Builtins: Throw a hard assertion on
lowering-cases for checked validator builtins that should never hit code
generation:
case SemIR::BuiltinFunctionKind::IntConvertFloatChecked: {
CARBON_CHECK(builtin_kind.IsCompTimeOnly(
context.sem_ir(), arg_ids,
context.sem_ir().insts().Get(inst_id).type_id()));
CARBON_FATAL("Missing constant value for call to comptime-only function");
}
Step 5: Standard Library Prelude Integration
Map the standard library primitive interfaces to your newly minted named
builtins under core/prelude/:
Primitive Mappings: Bind Carbon methods directly to string-literal
builtin equivalents:
fn Convert[self: Self]() -> Float(To) = "int.convert_float";
Strict Orphan Rule Compliance: Carbon's orphan rules prohibit
implementing interfaces where neither the type nor the interface is locally
defined in the backing source module.
- Literal Conversions: Literal types (like
FloatLiteral,
IntLiteral) do not have backing Carbon source files. Therefore, an
impl of UnsafeAs (which is defined in as.carbon) between two
literal types must reside inside as.carbon itself.
- Sized Conversions: Implementations targeting sized primitives (e.g.
Int(N), Float(N)) must reside in their respective type source files
(such as int.carbon or
float.carbon) where the
backing target type resides to prevent duplicate symbols and structural
recursion loops.
High-Fidelity Validation & Test Authoring
Follow the Toolchain tests skill with specialized
patterns for builtins:
1. Checker Builtin File Splits
Create validation splits under
toolchain/check/testdata/builtins/:
- Test Naming Convention: All tests under
toolchain/check/testdata/builtins/
must be named after the builtin they are testing, replacing
. characters
in the builtin name with / (directories). For example, a test for the
builtin "char_literal.convert" must be located at
toolchain/check/testdata/builtins/char_literal/convert.carbon.
- Minimal Prelude & Direct Call Isolation: Builtin tests must not test
the prelude library or operators. They must use the minimal primitive
prelude (
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/primitives.carbon) or a smaller
prelude, and explicitly declare and call the builtin functions under test
directly (e.g., fn Add(a: f64, b: f64) -> f64 = "float.add";). This
isolates the testing of compiler builtins from the library prelude.
- Min-Prelude Limitations: Standard operators (like
+, -, /, <,
etc.) are not available in minimized preludes because the core operators
library isn't imported. To write tests with a minimal footprint, call
primitive builtins directly (e.g. float.negate, float.div) inside your
test code to build expressions.
- Canonicalized Float Comparison: In SemIR, real literal representations
with identical mathematical values can result in mismatched
RealId objects
based on spelling variations. Verify compile-time constant conversions using
canonicalized comparison functions (e.g. passing converted results through
Expect(X as f64)) to completely avoid spelling mismatches in expected
outputs.
- Locals Bypass: If validating generic implicit conversions, compile-time
arguments cannot take local runtime variable parameters. Validate
compile-time conversions by passing literal constants directly, and sized
variable implicit conversions at runtime.
2. Machine Codegen Lowering Splits
Create testing splits under
toolchain/lower/testdata/builtins/:
- Emplace a simple carbon binding to the tested builtin.
- Confirm matching LLVM metadata target definitions are mapped precisely
(e.g., matching
sitofp i32 %a to float, fptosi float %a to i32).
1---2name: builtin-functions3description: Instructions for registering, mapping, constant evaluating, and lowering builtin functions in the Carbon toolchain.4---56# Builtin Functions in the Carbon Toolchain78<!--9Part of the Carbon Language project, under the Apache License v2.0 with LLVM10Exceptions. See /LICENSE for license information.11SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception12-->1314Builtin functions are compiler-recognized primitives mapping directly from15Carbon code expressions (via standard prelude bindings) to optimized backend16execution. This document defines the complete structural workflow, C++ patterns,17constant evaluation logic, machine lowering mechanics, library bindings, and18validation strategies required to implement builtin functions in the Carbon19compiler.2021---2223## Technical Flow & Lifecycle2425```mermaid26graph TD27 Src[Carbon Source Code] -->|Prelude Map| Sem[Semantic Analysis / SemIR]28 Sem -->|Signature Constraint| Sig[builtin_function_kind.cpp]29 Sem -->|Phase Evaluation| Eval[eval.cpp Constant Interpreter]30 Sem -->|Machine Codegen| Lower[handle_call.cpp LLVM Lowering]31 Eval -->|Diagnostics| Diag[diagnostics/kind.def]32 Lower -->|Native Instructions| LLVM[LLVM IR Generation]33```3435Adding a builtin function involves a 5-step integration:36371. **Define the Builtin Kind**: Register the enum in38 [builtin_function_kind.def](../../../toolchain/sem_ir/builtin_function_kind.def).392. **Signature & Compile-Time Registry**: Declare the mapping name, parameter40 constraints, and compile-time evaluation residency in41 [builtin_function_kind.cpp](../../../toolchain/sem_ir/builtin_function_kind.cpp).423. **Compile-Time Interpreter Support**: Wire constant evaluation hooks and43 bounds/exception diagnostics in44 [eval.cpp](../../../toolchain/check/eval.cpp).454. **LLVM IR Lowering Support**: Connect target machine generation in46 [handle_call.cpp](../../../toolchain/lower/handle_call.cpp).475. **Prelude Library Mapping**: Bind primitive interfaces to named builtins48 under [core/prelude/](../../../core/prelude/).4950---5152## Detailed Step-by-Step Implementation Guide5354### Step 1: Kind Definition & Registration5556Register your builtin function name using the X-macro in57[builtin_function_kind.def](../../../toolchain/sem_ir/builtin_function_kind.def):5859```cpp60// toolchain/sem_ir/builtin_function_kind.def6162// Converts an integer type to a floating-point type.63CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(IntConvertFloat)64```6566### Step 2: Signature Validation & Compile-Time Residence6768Inside69[builtin_function_kind.cpp](../../../toolchain/sem_ir/builtin_function_kind.cpp):70711. **Define Parameter Constraints**: If the parameter requires novel72 constraints (e.g. "must be a float type"), define a template constraint73 struct checking the matching `SemIR` type instruction (such as `FloatType`74 or `FloatLiteralType`). Use pre-established semantic helpers:7576 - `TypeParam<I, T>`: Ensures different parameters resolve to identical77 type structures (e.g., generic constraint matching).78 - `AnyInt`, `AnyFloat`, `AnySizedInt`, `AnySizedFloat`, `CharCompatible`,79 `StdInitializerList`, `NoReturn`.80812. **Map Literal Name & Register Constraint Signature**: Declare a82 `BuiltinInfo` constant inside `namespace BuiltinFunctionInfo` matching the83 macro-defined name:8485 ```cpp86 // toolchain/sem_ir/builtin_function_kind.cpp8788 constexpr BuiltinInfo IntConvertFloat = {89 "int.convert_float", ValidateSignature<auto(AnyInt)->AnyFloat>};90 ```91923. **Establish Compile-Time Residency Status**: Update93 `BuiltinFunctionKind::IsCompTimeOnly` to determine if a call requires94 compile-time evaluation:95 - **Checked/Diagnostics Primitives**: Return `true` immediately. Runtime96 lowering of these is illegal (e.g. `IntConvertFloatChecked`).97 - **Runtime Primitives**: Return98 `AnyLiteralTypes(sem_ir, arg_ids, return_type_id)` to enforce that99 expressions involving unsized literal values (like `IntLiteral` or100 `FloatLiteral`) are evaluated exclusively at compile-time (as they lack101 runtime representation).102103---104105### Step 3: Constant Evaluation Support106107Wire the interpreter inside [eval.cpp](../../../toolchain/check/eval.cpp) to108execute compile-time computations:1091101. **Implement Constant Evaluation Logic**:111112 - Handle the builtin case inside `MakeConstantForBuiltinCall` (which113 processes the compile-time execution of the call).114 - Confirm type validation phase is `Phase::Concrete` to reject incomplete115 bindings:116117 ```cpp118 case SemIR::BuiltinFunctionKind::IntConvertFloat: {119 if (phase != Phase::Concrete) {120 return MakeConstantResult(context, call, phase);121 }122 return PerformIntToFloatConvert(context, loc_id, arg_ids[0], call.type_id,123 /*require_exact=*/false);124 }125 ```126127 - Extract inputs safely from local value stores (e.g.128 `context.ints().Get(arg.int_id)` or129 `context.floats().Get(arg.float_id)`).130 - Leverage high-precision LLVM mathematical structures (`llvm::APInt`,131 `llvm::APFloat`, `llvm::APSInt`) to handle custom bits and signedness132 safely.1331342. **Diagnose Invalid Parameters or Exceptions**:135136 - Define compile-time diagnostics inside137 [kind.def](../../../toolchain/diagnostics/kind.def):138139 ```cpp140 // toolchain/diagnostics/kind.def141 CARBON_DIAGNOSTIC_KIND(IntTooLargeForFloatType)142 ```143144 - Emplace localized diagnostic formatting messages where they are caught145 in `eval.cpp`:146147 ```cpp148 CARBON_DIAGNOSTIC(IntTooLargeForFloatType, Error,149 "integer value {0} too large for floating-point type {1}",150 llvm::APSInt, SemIR::TypeId);151 context.emitter().Emit(loc_id, IntTooLargeForFloatType, val, dest_type_id);152 ```153154 - Return `SemIR::ErrorInst::ConstantId` to gracefully abort invalid155 constant generation rather than crashing the compiler.1561573. **Fast-Path Range Limits**:158 - Before evaluating expensive math operations on giant exponents (e.g.159 `1.0e1000000`), executing range limits check against `dest_width + 64`160 (sized) or `IntStore::MaxIntWidth` (unsized) is mandatory to prevent161 out-of-bounds calculations and compile-time memory exhaustion.162163---164165### Step 4: Machine Code Generation (LLVM Lowering)166167Inside [handle_call.cpp](../../../toolchain/lower/handle_call.cpp):1681691. **Map to Native LLVM Instructions**: For runtime-eligible builtins, map the170 call inside `HandleBuiltinCall` to native LLVM IR builder methods:171172 ```cpp173 case SemIR::BuiltinFunctionKind::IntConvertFloat: {174 auto* operand = context.GetValue(arg_ids[0]);175 auto* dest_type = context.GetTypeOfInst(inst_id);176 bool is_signed = IsSignedInt(context, arg_ids[0]);177 context.SetLocal(178 inst_id, is_signed179 ? context.builder().CreateSIToFP(operand, dest_type)180 : context.builder().CreateUIToFP(operand, dest_type));181 return;182 }183 ```1841852. **Assert on Compile-Time-Only Builtins**: Throw a hard assertion on186 lowering-cases for checked validator builtins that should never hit code187 generation:188189 ```cpp190 case SemIR::BuiltinFunctionKind::IntConvertFloatChecked: {191 CARBON_CHECK(builtin_kind.IsCompTimeOnly(192 context.sem_ir(), arg_ids,193 context.sem_ir().insts().Get(inst_id).type_id()));194 CARBON_FATAL("Missing constant value for call to comptime-only function");195 }196 ```197198---199200### Step 5: Standard Library Prelude Integration201202Map the standard library primitive interfaces to your newly minted named203builtins under [core/prelude/](../../../core/prelude/):204205- **Primitive Mappings**: Bind Carbon methods directly to string-literal206 builtin equivalents:207208 ```carbon209 fn Convert[self: Self]() -> Float(To) = "int.convert_float";210 ```211212- **Strict Orphan Rule Compliance**: Carbon's orphan rules prohibit213 implementing interfaces where neither the type nor the interface is locally214 defined in the backing source module.215 - **Literal Conversions**: Literal types (like `FloatLiteral`,216 `IntLiteral`) do not have backing Carbon source files. Therefore, an217 `impl` of `UnsafeAs` (which is defined in `as.carbon`) between two218 literal types must reside inside `as.carbon` itself.219 - **Sized Conversions**: Implementations targeting sized primitives (e.g.220 `Int(N)`, `Float(N)`) must reside in their respective type source files221 (such as [int.carbon](../../../core/prelude/types/int.carbon) or222 [float.carbon](../../../core/prelude/types/float.carbon)) where the223 backing target type resides to prevent duplicate symbols and structural224 recursion loops.225226---227228## High-Fidelity Validation & Test Authoring229230Follow the [Toolchain tests](../toolchain_tests/SKILL.md) skill with specialized231patterns for builtins:232233### 1. Checker Builtin File Splits234235Create validation splits under236[toolchain/check/testdata/builtins/](../../../toolchain/check/testdata/builtins/):237238- **Test Naming Convention**: All tests under239 [toolchain/check/testdata/builtins/](../../../toolchain/check/testdata/builtins/)240 must be named after the builtin they are testing, replacing `.` characters241 in the builtin name with `/` (directories). For example, a test for the242 builtin `"char_literal.convert"` must be located at243 `toolchain/check/testdata/builtins/char_literal/convert.carbon`.244- **Minimal Prelude & Direct Call Isolation**: Builtin tests must **not** test245 the prelude library or operators. They must use the minimal primitive246 prelude (`// INCLUDE-FILE:247 toolchain/testing/testdata/min_prelude/primitives.carbon`) or a smaller248 prelude, and explicitly declare and call the builtin functions under test249 directly (e.g., `fn Add(a: f64, b: f64) -> f64 = "float.add";`). This250 isolates the testing of compiler builtins from the library prelude.251- **Min-Prelude Limitations**: Standard operators (like `+`, `-`, `/`, `<`,252 etc.) are **not** available in minimized preludes because the core operators253 library isn't imported. To write tests with a minimal footprint, call254 primitive builtins directly (e.g. `float.negate`, `float.div`) inside your255 test code to build expressions.256- **Canonicalized Float Comparison**: In SemIR, real literal representations257 with identical mathematical values can result in mismatched `RealId` objects258 based on spelling variations. Verify compile-time constant conversions using259 canonicalized comparison functions (e.g. passing converted results through260 `Expect(X as f64)`) to completely avoid spelling mismatches in expected261 outputs.262- **Locals Bypass**: If validating generic implicit conversions, compile-time263 arguments cannot take local runtime variable parameters. Validate264 compile-time conversions by passing literal constants directly, and sized265 variable implicit conversions at runtime.266267### 2. Machine Codegen Lowering Splits268269Create testing splits under270[toolchain/lower/testdata/builtins/](../../../toolchain/lower/testdata/builtins/):271272- Emplace a simple carbon binding to the tested builtin.273- Confirm matching LLVM metadata target definitions are mapped precisely274 (e.g., matching `sitofp i32 %a to float`, `fptosi float %a to i32`).