.agents/skills/builtins/SKILL.md
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.
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:
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)
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:
true immediately. Runtime
lowering of these is illegal (e.g. IntConvertFloatChecked).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).Wire the interpreter inside eval.cpp to execute compile-time computations:
Implement Constant Evaluation Logic:
MakeConstantForBuiltinCall (which
processes the compile-time execution of the call).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);
}
context.ints().Get(arg.int_id) or context.floats().Get(arg.float_id)).llvm::APInt,
llvm::APFloat, llvm::APSInt) to handle custom bits and signedness
safely.Diagnose Invalid Parameters or Exceptions:
// toolchain/diagnostics/kind.def
CARBON_DIAGNOSTIC_KIND(IntTooLargeForFloatType)
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);
SemIR::ErrorInst::ConstantId to gracefully abort invalid constant
generation rather than crashing the compiler.Fast-Path Range Limits:
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.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");
}
Map the standard library primitive interfaces to your newly minted named builtins under core/prelude/:
fn Convert[self: Self]() -> Float(To) = "int.convert_float";
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.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.Follow the Toolchain tests skill with specialized patterns for builtins:
Create validation splits under toolchain/check/testdata/builtins/:
+, -, /, <,
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.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.Create testing splits under toolchain/lower/testdata/builtins/:
sitofp i32 %a to float, fptosi float %a to i32).