Builtins
Plan 11 step 36 (2026-10-07): the production builtin bridge. Builtins are
erlang functions the runtime implements in C++. The runtime registers them
by module, function and arity; generated code reaches them directly, through
dynamic calls and as fun values.
Which builtins exist
The bridge catalog abi::v1::bridge_builtins
(builtins.hpp) lists every
builtin both the compiler and the runtime know:
- the guard BIFs: type tests (
is_atom/1…is_tuple/1,is_function/1,2),abs/1,bit_size/1,byte_size/1,ceil/1,element/2,float/1,floor/1,hd/1,length/1,map_get/2,map_size/1,is_map_key/2,max/2,min/2,round/1,size/1,tl/1,trunc/1,tuple_size/1,binary_part/2,3; - the operators as functions: comparisons,
not/1,and/2,or/2,xor/2, arithmetic and bitwise operators (erlang:'+'/1,2…); display/1,halt/0,1,error/1,2,3,exit/1,throw/1,raise/3andfunction_exported/3;- term access (plan step 37):
setelement/3,make_tuple/2,3,tuple_to_list/1,list_to_tuple/1and the list operators'++'/2and'--'/2(A ++ B,A -- Blower to them); - conversions (plan step 38):
atom_to_list/1,list_to_atom/1,integer_to_list/1,2,list_to_integer/1,2,float_to_list/1,2,binary_to_list/1,list_to_binary/1,iolist_to_binary/1(term_to_binary/1is not selected); - console output (plan step 40):
io:format/1,2andio:put_chars/1, the first builtins of another module (io); - process identities (plan step 42):
self/0,make_ref/0,pid_to_list/1andref_to_list/1(pids and references); - processes (plan step 43):
spawn/1,3andis_process_alive/1(processes); links and exit signals (step 48):spawn_link/1,3,link/1,unlink/1,exit/2,exit_signal/2,process_flag/2(links); monitors (step 49):spawn_monitor/1,3,monitor/2,demonitor/1,2(monitors); registered names (step 50):register/2,unregister/1,whereis/1,registered/0(registered names); - messages (plan step 45):
'!'/2(the!operator) andsend/2(messages).
Entries are only appended: an entry's index is the number generated code
passes to the bridge service. A qualified call of a catalog builtin of another
module (io:format(F, A)) also calls the bridge. Other erlang functions keep
their diagnostics:
a direct call of an unknown one is unknown module erlang, fun erlang:F/A or
fun F/A of a guard BIF outside the catalog (node/0) and
fun erlang:apply/2,3 report the unavailable dynamic calls capability.
How calls reach them
| Source | Path |
|---|---|
abs(X), X + Y, erlang:display(X), halt(), error(R) | Inline services, as before the bridge |
erlang:function_exported(M, F, A), setelement(I, T, V), A ++ B, A -- B, length(L) in a body (catalog builtins without an inline service) | Entered like a function: CLAUSE_builtin_frame_v1(context, index) gives the builtin's frame, the arguments go in the registers (portions) |
fun abs/1, fun erlang:'+'/2 | External fun erlang:F/A; registration binds it to the builtin |
M:F(Args), apply(M, F, Args), runtime fun M:F/A | The code server finds a module's export first, then a builtin |
fun F/Aof an auto-imported builtin the module neither defines nor suppresses (-compile({no_auto_import, ...})) is the external funerlang:F/A, as in OTP:fun abs/1 =:= fun erlang:abs/1and it prints asfun erlang:abs/1.halt/0,1,setelement/3,tuple_to_list/1,list_to_tuple/1, the conversions, the process builtins and the port builtinsopen_port/2,port_close/1,port_command/2,3,port_connect/2,port_control/3,port_to_list/1,list_to_port/1are auto-imported like OTP's;display/1,raise/3,function_exported/3,make_tuple/2,3,port_info/1,2,port_call/2,3andports/0need theerlang:prefix (ports).- A builtin has a
FrameDescriptorwith a null body (BuiltinFrame). Entering it (CLAUSE_enter_v1,CLAUSE_tail_v1) pushes no frame: the builtin runs on the registers and its result returns into the caller's body, so a builtin in tail position returns to the caller's caller. - Errors are the ones the inline lowering raises in a body:
badarg,badarithfor arithmetic,system_limit,{badmap, M},{badkey, K};raise/3with an invalid class or stack returnsbadarg. They go through the checked failure channel like every service error. - Term access follows OTP's
badargrules:setelement/3needs a small integer index within the tuple;make_tuple/2,3a small size in 0..16,777,215, andmake_tuple/3a proper list of{Index, Value}pairs within the size (later pairs win);list_to_tuple/1a proper list;A ++ Ba proper listA([] ++ BisBfor anyB, a non-listBends the result);A -- Btwo proper lists, each element ofBremoving the first exactly equal (=:=) element ofA, in O((n + m) log m). - Conversions follow OTP:
list_to_atom/1takes a proper list of code points (no surrogates); a 256th character issystem_limitbefore it is checked. A full atom table (--max-atoms) stops the program as a runtime failure (resource_limit, exit 70).integer_to_list/2andlist_to_integer/2take a small base in 2..36; digits print uppercase and parse in either case.list_to_integeraccepts one optional sign, skips leading zeros, needs a digit, and raisessystem_limitfor more than 1,262,611 significant decimal digits (or 4,194,304 in any base) once its first digits are valid, and for a value past the integer limit.float_to_list/1is"%.20e";/2options apply in order, the last format winning:{scientific, D}("%.*e", negative D is 6),{decimals, D}(D >= 0; fixed with OTP's own rounding below 2^53 and 19 decimals,compacttrims trailing zeros, also of an integer with{decimals, 0}above 2^53),short(shortest round-trip digits in OTP's fixed/scientific choice). Text of 256 bytes or more isbadarg.binary_to_list/1needs a binary;list_to_binary/1a list andiolist_to_binary/1a list or binary of bytes, binaries and nested lists, each list ending in[]or a binary.
function_exported(M, F, A)raisesbadargunlessMandFare atoms andAa small integer; it is true when a module of the program exportsM:F/AorM:F/Ais a registered builtin.
Portions
Plan 11 step 43A (2026-10-08): builtins whose work grows with a list or binary argument run in bounded portions, like OTP's trapping BIFs, so a long builtin cannot keep other processes from running (processes).
- Every bridge builtin a body calls is entered like a function and spends a
reduction. A portion may do
WORK_PER_REDUCTION(16) units of work per reduction left in the time slice (at least one reduction's worth): list cells walked or built, comparisons, bytes. It pays for them from the slice. - A builtin with work left traps:
ProcessStack::trapnames a continuation frame and puts the builtin's state terms in the registers, which stay roots; native state (bytes, sort positions, term words kept as roots) lives in the process stack'sTrapState. The process yields and resumes at the continuation in a later slice. Collections between portions rewrite the registers and state words like other roots. - Results, errors and evaluation order are the same as running to completion. An error found late (an improper tail) is raised when the walk reaches it.
- Host calls (
call_builtin) continue every trap at once.
| Builtin | Portions |
|---|---|
length/1 in a body | Counts cells; in a guard it stays the inline service, as OTP's guard BIF does not trap |
A ++ B | Collects A's elements, then builds the copy onto B from its end |
A -- B | Collects B, sorts it by exact order (bottom-up merge sort), scans A with a binary search per element, then builds the kept elements; when nothing is removed the result is A itself |
binary_to_list/1 | Builds the list from the binary's end |
list_to_binary/1, iolist_to_binary/1 | Walks the iolist depth first, then makes the binary at once |
Other builtins run to completion. The tuple builtins (setelement/3,
make_tuple/2,3, tuple_to_list/1, list_to_tuple/1) do as in OTP: their
work is bounded by the tuple arity limit. The remaining conversions read
inputs bounded by the atom, integer and float limits. Formatting with
io:format/1,2 runs to completion (differences). Inline
services of loops (a comprehension's final reverse) run to completion as
part of the loop.
Typed builtins
Plan 11 step 41 (2026-10-07): builtins that check their own arguments are C++
functions of typed parameters (typed.hpp),
Result Function(ProcessContext &, Parameters...), registered with
typed_entry<Function>(module, name) (the arity is the parameter count).
- The adapter admits every argument word in order (a word this process does
not own is the failure it is, never
badarg), then converts each to its parameter type; a mismatch raisesbadargand the function does not run. - Parameter types:
Term(any term, the generic fallback),std::int64_t(a small integer),detail::Integer(any integer),double(a float),ListArgument(a proper list and its elements),TupleArgument,BinaryArgument(bytes of a binary),AtomArgument(its spelling). - Results:
Term,TermResult<Term>(a failed construction is a runtime failure),BuiltinResult<Term>(std::expectedwith aBuiltinFailure), or a rawWordthe function published itself. A function may also throwBuiltinFailure(an Erlang error such asbadargorsystem_limit, or a term access failure);call_builtinturns every other C++ exception intoout_of_memoryorinternal_error, so none crosses the generated-code ABI. - The term-access, conversion and io families,
binary_part/2andfunction_exported/3are typed. The othererlangbuiltins pass their argument words unconverted to the inline services generated code also calls, which admit them; they stay word-levelBuiltinBodyadapters.
Registration
BuiltinRegistry(builtin_registry.hpp), owned by theCodeServer, maps exact module/function/arity to aBuiltinFrame.addtakes a batch ofBuiltinEntrys and registers all or none: an empty name, more than 255 arguments, a missing implementation, or a name already registered (or repeated in the batch) rejects the batch with nothing kept.- Runtime startup registers every table of
production_builtins()(erlang_builtins(),term_access_builtins(),conversion_builtins(),io_builtins()), most entries made bytyped_entry, which together cover the catalog; later families add their own tables. - A body reads exactly its arity of argument words and records errors in the
checked channel; host exceptions become
out_of_memoryorinternal_errorfailures. - The older host path
abi::v1::dispatch_builtin(host-registered native modules by name, runtime) is separate and unchanged.
Clause