Term representations
Admitted kinds: atoms/booleans, arbitrary integers, finite binary64 floats, tuples, proper/improper lists and strings, maps, bitstrings and ordinary tuple records; native records (native records); funs (function values); local pids, ports and references (below, ports). Word encodings are in abi.md.
Ownership
- Compound values live in their process heap (runtime). Process pointers always name object starts. Admission checks ownership: the word points, word-aligned, below the top of the process's heap block or one of its fragments, and the header (or cons cell) there matches its tag (admission). Foreign and stale words are rejected without any load. Kind and extent are decoded from the header.
- A host
Termfor a heap value is a raw tagged word, valid until its heap's next collection (roots); it does not pin heap storage. After context teardown, access returnsexpired_context; after a later collection,stale_term. Terms are always safe to destroy. - Construction validates children, reserves, initializes, then publishes in one step; failure rolls back backing and counters.
Term::from_word(word)admits only owner-independent immediates;Term::from_word(word, context)also admits atoms and heap terms of that context. Same-heap handoff keeps identity.copy_to/ProcessHeap::addcopy a graph of another process of the same runtime with its sharing; term factories refuse foreign inputs withwrong_owner(copying between heaps).- Cells live until an explicit collection finds them unreachable, or until heap teardown.
Atoms
- Per-runtime
AtomStorage, lazily interned by exact UTF-8 spelling, no normalization or eviction.RuntimeOptions::max_atoms1..2^26, default 2^20; module/export names count. Existing spellings succeed at capacity. - Up to 255 Unicode scalars; empty, NUL and supplementary characters allowed; malformed/overlong/surrogate UTF-8 rejected.
- Payloads come from a process-wide counter so a foreign runtime's atom word is detectable. 32-bit processes have 2^26 lifetime identities in total.
- Host atom Terms pin the spelling, surviving runtime teardown. Moving an atom
between runtimes means interning
atom_utf8()in the destination. - Booleans are the atoms
true/false. - Interning and lookup are safe from concurrent scheduler workers (threads); a spelling keeps one word.
Integers
- Values in the target's 28/60-bit payload are immediates; larger ones are immutable sign/magnitude cells. Zero and small values always normalize to immediates. No host-width narrowing of literals.
- Generated
+,-,*try an inline fast path on two immediates (double-width compute with explicit bounds), otherwise call the runtime service. divtruncates toward zero;remtakes the dividend's sign; bitwise ops use infinite two's complement; negative shift counts reverse direction; huge right shifts saturate to 0 or -1.- Errors: wrong operands and zero divisor →
badarith;abs/1→badarg. - Limits: as ERTS, a magnitude of at most
BIG_ARITY_MAXwords: 4,194,240 bits on 64-bit targets (65,535 words), 4,194,272 on 32-bit (131,071 words); decimal text follows (1,262,593 and 1,262,602 digits). A larger arithmetic result raiseserror:system_limitin a body (ValueOutcome::system_limit, ABI) and fails a guard; an integer segment extracting a larger value does not match. The compiler rejects a literal past 4,194,240 bits (illegal integer, as OTP's scanner) and a constant pattern past it (illegal pattern).
Floats
- IEEE binary64 bits pass to the runtime as eight network-order bytes; NaN and infinity are rejected. No fast-math.
+ - *stay exact on two integers; any float operand uses binary64./always converts both. Nonfinite results and zero divisors →badarith.float/1rounds to nearest-even;round/1ties away from zero;trunc,floor,ceilreturn arbitrary integers. Bad operands →badarg.- Exact equality distinguishes
1from1.0and0.0from-0.0; numeric comparison compares the float's exact integer part and fraction, never rounding the integer.min/maxreturn the first operand on ties.
Tuples, lists, strings
- Tuple: arity header + fields. Cons: head + tail words.
{}and[]are immediates. Strings are lists of code points. Lists have no length cap beyond memory (an optional heap budget included). Tuples hold up to 16,777,215 elements (MAX_TUPLE_ARITY, OTP'sMAX_ARITYVAL); constructors report a larger one asresource_limit, builtins will raisebadarg. - Services:
hd,tl,length,tuple_size,size, one-basedelement.
Maps
- Immutable tables sorted by exact key order. Duplicate construction keys keep
the last value. Construction sorts the keys (O(n log n) comparisons; already
ascending keys are only checked); updates insert by binary search. No size
or work cap beyond memory, as in OTP; on 32-bit targets the header's word
count bounds a map at 2^24 - 1 entries (
resource_limit). Integer and float keys differ (also0.0vs-0.0, also nested). K := Vupdates require the key;K => Vinserts or replaces. Updates stage a new table and publish once.- Body errors:
{badmap, M},{badkey, K}; guards reject instead. - Services:
is_map,map_size,map_get,is_map_key, construction, update.
Bitstrings
- Packed MSB-first with exact bit length, zeroed padding. Up to 64 bytes live inline in a heap binary sized to the data; larger values use a shared immutable buffer outside the heap, viewed by off-heap binary cells that extracted tails share. The buffer is charged once to the creating process. There is no size cap beyond an optional process heap budget; integer segments are written without building an integer as wide as the segment.
- Construction stages all segments before publishing. Integer segments truncate; native endianness comes from the target data layout.
- Float segments: widths 16/32/64; construction may encode infinity, but
matching rejects infinite/NaN fields. Zero-width float matches extract
0.0. - UTF-8/16/32 segments validate scalars, surrogates and truncation.
- Matching advances an explicit bit cursor only on success;
:allas an explicit size is invalid. - Services:
is_binary,is_bitstring,bit_size,byte_size(rounds up),size(rounds down),binary_part/2,3. Errors →badarg.
Records
- Ordinary records expand to tuples
{Tag, Fields...}. Declarations must precede use; duplicates, unknown fields, forward/self references and invalid wildcard fields are errors. - Construction evaluates fields in declaration order: explicit value, else
_ = Vwildcard default, else declared default, elseundefined. Each default is evaluated separately per use. - Patterns check arity and tag, then only the listed fields.
#r.fis the one-based index (tag at 1). - Field access checks arity and tag; failure is
{badrecord, V}in bodies and rejection in guards. is_record(V, r)uses the declared arity.is_record/3needs an atom tag and integer arity (non-positive → false; wrong types →badarg); an atom third argument is the native-record query and returns false. Guards require literal arguments.- Update
Expr#r{f = V, ...}evaluates the new values in source order, thenExpr, then checks arity and tag ({badrecord, Value}on mismatch, also forExpr#r{}) and builds a new tuple; the other fields are copied._ = Vis rejected in updates; updates are illegal in patterns and guards. record_info(fields | size, r)expands at compile time to the field-name list or the tuple size. Both arguments must be literal atoms andra tuple record declared earlier; it is illegal in guards, and a localrecord_info/2is rejected as already defined.- Native records: native records (local, qualified, imported and anonymous forms).
Pids and references
Plan 11 step 42. self/0 returns the calling process's pid, make_ref/0 a
new reference; pid_to_list/1 and ref_to_list/1 return their text.
- A pid is an immediate word (low four bits
0x3) holding the process's number. Numbers come from one process-wide sequence and are never reused, so a runtime admits a pid word only when it issued that number: a forged word (never issued) or another runtime's pid iswrong_owner. The pid of an exited process stays a valid term, as in OTP. 32-bit targets have 2^28 numbers per program run, 64-bit targets 2^60; creating a process past them fails withresource_limit. - A port (plan step 57B) is an immediate word (low four bits
0x7) holding its number from its own never-reused sequence, admitted like a pid; it prints as#Port<0.N>and orders between funs and pids (ports). - A reference is a heap cell (
reference: header plus an untraced 64-bit number) admitted like every heap term: only in its own process, stale after a collection of a hostTerm, copied by value between processes. Numbers come from one process-wide counter, so every reference of a program run is unique. - Printing follows OTP's local identities: a pid as
<0.N.S>(N the low 28 bits of its number, S the rest), a reference as#Ref<0.A.B.C>(C the low 18 bits of its number, B the next 32, A the rest), in both~wand display styles. Pids order by number, references by number, so later references of a program order after earlier ones.
Comparison and order
Iterative with no work cap, as in OTP: only memory for pending pairs bounds a comparison, map key searches included, and identical words are equal without a walk. Byte-aligned bitstrings compare whole bytes at once. Order: numbers < atoms < references < funs < pids < tuples < native records < maps < nil < lists < bitstrings (funs order among themselves). Atoms compare by UTF-8 spelling (code-point order); tuples by arity then fields; maps by size, then keys, then values; bitstrings by logical bits.
Printing
format_term (output.hpp)
renders any admitted term in one of two OTP styles. Integers, tuples (records
are tuples) and nesting look the same in both.
~w (TermStyle::write) | erlang:display/1 (TermStyle::display) | |
|---|---|---|
| Atoms | Quoted unless a Latin-1 lowercase letter starts it and name characters (with @) follow; reserved words and maybe/else are quoted; beyond Latin-1 escapes as \x{H} | Quoted unless a Latin-1 lowercase letter starts it and alphanumerics or _ follow; reserved words and @ get no special rule; UTF-8 kept |
| Floats | Shortest round trip in OTP layout: 0.1, 100.0, 1.0e16, 1.5e-7 | C %.6e: 1.500000e+00 |
| Lists | Elements: [104,105], [1,2|3] | A flat list of printable Latin-1 bytes prints as "hi" (raw bytes; only \n and " escaped) |
| Bitstrings | <<1,2,5:3>> | A printable ASCII binary prints as <<"hi">>, others as ~w |
| Maps | #{k => v,k2 => v2} | #{k=>v,k2=>v2} |
- Maps print in map-key order (
maps:iterator(M, ordered), as OTP~kw). OTP's default~wanderlang:display/1follow its internal layout instead: atom-table order for atom keys of small maps (it varies between VM runs) and hash order above 32 keys. Clause does not reproduce that order. - Rendering is iterative, so depth is limited only by the term. Text is capped
at 64 MiB by default; exceeding it (for example a widely shared subterm) fails
with
resource_limitand returns no partial text. - Goldens:
runtime_printingcompares both styles with OTP for 9,542 values (all corpus results plus authored edge cases); display rows whose OTP map order is internal are skipped (fixtures).
Clause