Clause
← All documentation

Semantic analysis

Runs after parsing in default compilation and --print-types; syntax-only actions skip it. Errors stop the affected batch before LLVM; diagnostics keep macro/include origins, and later inputs are still diagnosed.

Module and call checks

Bindings

Each binding has a function-relative identity clause[N].local[M]. Occurrences are definitions, reads or exact-equality checks, tagged with head/guard/body context. Analysis is deterministic.

Walks are iterative with a module budget of 1,000,000 work units. Exhaustion or any semantic error clears the module's binding and normalization tables.

Types and specifications

A private type graph represents all parsed type forms independently of runtime layout: singletons, ranges, containers, map field roles, function products and unresolved applications. Unions flatten and deduplicate; term() is top and none() bottom. Defaults: 16,384 nodes, 16 union members, 100,000 work items per translation. Exhaustion widens to term() with a visible flag and never narrows a representation.

Declared metadata (-type, -opaque, -nominal, -export_type, -spec, -callback, -optional_callbacks) follows the OTP typespec reference and pinned erl_lint/erl_internal/erl_types behavior:

Inference

Inference is separate from declared types and never trusts specs.

Inference domain

Decision of plan 11 step 58A (semantic/types/lattice). A fact is a set of values a variable or result can have. Facts join where control flow meets (clauses, branches) and widen between the rounds of a recursive component; every budget below widens soundly to a larger set, never rejects a program. Facts print as Erlang types, categories by their built-in names.

FactPrintedJoinBudget and widening
Nothingnone()IdentityA function that never returns stays none()
Anythingterm()Absorbs every factdynamic() and any() are term()
Integers42, 1 | 3 | 7Union of singletonsMore than 8 singletons become their range
Integer range1..10, 0..255Smallest range holding bothA bound that moved between rounds goes to the next threshold: a lower one to 1, then 0, then unbounded; an upper one to -1, then unbounded
Unbounded integerspos_integer() (1 and up), non_neg_integer() (0 and up), neg_integer() (-1 and down), integer()Smallest interval holding both, printed by its category—
Floatsfloat()——
Numbersnumber()A range or category of integers joined with float()Singleton integers with float() stay 1 | float()
Atomsok, error | ok, boolean()Union of singletons; exactly false and true print boolean()More than 8 singletons become atom()
Identifierspid(), port(), reference()——
Tuples{ok, 1}, tuple(), #point{x :: 0, y :: _}Tuples of the same size whose first elements are not two different atoms (their tag) join element by element; others stay separate membersMore than 16 elements become tuple() unless every element is known; more than 8 separate shapes become tuple(). A tuple of a visible tuple record's name and size prints as the record (step 58J)
Lists[], [T], [T, ...], nonempty_improper_list(H, T), [1, a], [a, b | T]Elements join; [] with a nonempty list gives a possibly empty one; improper lists join heads and tails. Lists of two or more known elements keep their positions (step 58J; a Clause notation, the type language has none, elements printed with | in parentheses): positional lists of one length join position by position, otherwise they join as plain listsA list of 0..1114111 (char()) prints string() or nonempty_string(); a possibly empty list of _ prints list()
Maps#{}, #{a := 1}, #{1..17 => a}, map()Maps with the same keys join value by value; maps of other keys join into one association of their joined keys and values (=>: any key may be missing, step 58J)More than 16 keys join into one association
Funsfun((term()) -> 1), fun()Funs of one arity join their results; other arities give fun()—
Bitstrings<<_:16>>, <<_:3, _:_*2>>, binary()The shorter size plus every difference of sizes as a unitBase and unit 0/8, 8/8, 0/1, 1/1 print binary(), nonempty_binary(), bitstring(), nonempty_bitstring()

Lowering consumes these facts. Generated IR never converts an integer to a heap pointer; each fallible service result is loaded only on its success path, and shape checks dominate extraction. Specialization policy: specialization.md.

--print-types

Prints each module of the batch (input/target order, library modules after them) as Erlang source (source printing) with what type inference found. Output is on stdout and is human-readable, not Erlang and not an interchange format. Warnings stay on stderr.

%% module "branches" source="branches.erl" target="" declared=complete inferred=complete
-module(branches).
-export([mixed/1]).

%% inferred: mixed(_) -> 1..2
mixed(X) ->
    case X of
        1 ->
            1;
        _ ->
            2
    end :: 1..2.

Inference expectations

tests/fixtures/inference/*.erl record what inference should find for each function, and what it finds today. Each module becomes the CTest inference_<module> (tests/compiler/inference/expectations.py):

%% expect: sum() -> 3
%% today: sum() -> _
sum() -> 1 + 2.

Printing types

semantic::types::type_source(graph, type) (semantic/types/printing) renders a type of the type graph in Erlang type syntax: _ for any term (term(), written by TERM_SOURCE for brevity; type syntax reads _ as any()), none(), atoms and integers, 1..5, {ok, T}, tuple(), [T], [T, ...], #{K => V, K := V}, #r{f :: T}, <<_:B, _:_*U>>, fun((A) -> R), A | B. A fun of several function types prints them in a Clause notation, fun((1) -> one; (_) -> other): Erlang type syntax has no overloaded fun type, and a union of fun types means something else. A union's integers print in value order where its first integer stands, consecutive ones as a range (1 | 2 | 3 | 5 prints 1..3 | 5; the fact keeps the singletons). Predefined erlang types drop their module; references to declared types stay named. A budget of nodes bounds the text; past it, and below 32 levels of nesting, ... stands in.

clau --print-types answer.erl client.erl
clau --print-types --project project.toml --target demo --verbose