Attribute annotations, representations, and branch weights
Added 2026-10-25Source: local draft in this repository (added after the documentation export)
Draft 0.1. This chapter defines the attribute annotation system: #[...]
annotations attached to AST nodes, their syntax, their ownership by either
the compiler or the macro machinery, and their identity rules. It then
defines the first attribute families: manual branch weights with the
cold/likely/unlikely wrappers, inline hints, and manual memory
layout, representation, and alignment control. MUST denotes a requirement
of this draft. Proposed choices are identified explicitly; unresolved items
are listed in Open decisions. The branch weight semantics in section 3
follow the weight mathematics of GHC proposal 744 (Branch Prediction
Hints, Ian Duncan), which this chapter treats as the reference for weight
algebra: relative weight spaces, marginalization through pattern
decomposition, and the simplifier reconciliation rules.
1 The attribute system
An attribute is a name with optional arguments written #[...] in prefix
position before the annotated construct:
attribute = "#[", attribute_name, [ attribute_args ], "]"attribute_name = name, { ".", name }attribute_args = "=" literal | literal, { ",", literal }Examples: #[inline], #[inline(always)], #[weight(2000)],
#[repr(align(16))], #[cold], #[meta(key = "value")].
1.1 Attachment points
Attributes MAY prefix the following constructs. An attribute on any other construct is a syntax error.
- Declarations:
data,record,signature,instance,foreignimport and export,upgrade,rewrite, and top-level function equations. - Data constructor declarations and record fields.
- Function clauses (between the function name and its first pattern, or
before an equation’s patterns),
casealternatives, and guards (after|, before the guard expression). - Expressions in branch positions: the branches of
if, and the alternatives ofcase. - Types in signatures where a representation attribute applies (section 4).
1.2 Ownership and validation
Attributes fall into two ownership classes:
- Compiler attributes are the ones defined in this chapter (and future compiler-defined families). The compiler MUST validate their arguments and their placement, and MUST reject an unknown attribute name that is not covered by the next point.
- Macro attributes are introduced or consumed by hygienic attribute
macros (see Modules and compilation specification
section 8). An attribute name a macro declares is valid wherever the
macro accepts it; the macro is responsible for its argument grammar. A
reserved
metanamespace (#[meta(...)]) passes through to tooling without semantic effect and is always accepted.
Attributes are not ordinary names: they do not participate in name resolution, cannot be shadowed, and a macro’s attribute names carry binding identity through expansion like any other identifier (hygiene). Attributes introduced by macro expansion MUST record expansion provenance in source maps, as required by the modules specification’s hygiene rule.
1.3 Semantics classes and identity
Every compiler attribute belongs to exactly one of two semantics classes,
and the class determines the identity rules of
Modules and compilation specification
section 4 and compiler/CONFORMANCE.md decision D9 (contract-only
interface identity):
- Semantic attributes change the meaning of the program’s representation or behavior at the ABI boundary: the representation and layout attributes of section 4. They MUST appear in the compiled interface’s contract projection: changing one changes the module’s content identity, and dependents MUST be rechecked.
- Hint attributes may only affect performance, never observable behavior: the weight attributes of section 3 and the inline attributes of section 5. They MUST NOT appear in the contract projection, MUST NOT affect type checking outcomes, and any optimizer that transforms a program MUST preserve or drop them without changing the program’s meaning. A program with incorrect hints produces the same values, only possibly slower.
Two attributes on the same construct with the same name are an error unless a macro declares a multi-attribute form.
2 Branch weights: the primitive
The primitive control-flow annotation is a manual weight on one alternative
of a decision point. The construct is #[weight(n)] where n is a
non-negative integer literal (representable in 32 bits). Weight attributes
are valid on case alternatives, function clauses, guards, and the two
branches of if.
2.1 Weight algebra
The following rules are normative and follow the reference weight mathematics.
- Relativity. Weights are relative within a single decision point
(one
case, one guard group, oneif). Only ratios matter:#[weight(2000)]and#[weight(1)]is the same assignment as#[weight(4000)]and#[weight(2)]. A weight has no meaning in relation to the weights of a different decision point. - Zero.
#[weight(0)]means negligible probability but not impossibility. It is the signal for cold-code placement (section 3.3); it does NOT make the branch dead code, and the compiler MUST NOT eliminate it. - All-or-nothing. If any alternative of a decision point carries a
weight, every alternative MUST carry one (directly or through a wrapper,
section 3). Partial annotation is ambiguous under relativity: when it is
detected, the compiler MUST drop ALL weights of that decision point and
report
incomplete-branch-weights. This is the reference design’s resolution of weight inference: inference never mixes with explicit weights (see Open decisions for the warning-level choice). - Storage. Weights are
Word32values attached to each alternative. In the compiler’s core representation the weight is a property of the alternative itself (aMaybe u32beside it), never a parallel list on the decision node, so transformations cannot separate them.
2.2 Pattern decomposition and marginalization
When a weighted multi-clause function compiles to nested case
expressions (one per pattern column), each alternative’s weight is the sum
of the weights of the clauses whose right-hand side is reachable through
that alternative. When every clause uses explicit constructor patterns,
this is exactly marginalization: treating clause weights as unnormalized
joint probabilities over the pattern columns, the outer alternative for
constructor c_i receives the marginal
$$w(c_i) = \sum_{j} w(c_i, p_j)$$
where $p_j$ ranges over the inner patterns paired with $c_i$, and each inner alternative keeps its clause weight (the conditional). The invariant is that the product of the weights along any root-to-clause path, with the inner weights read as conditionals, reproduces the clause’s flat weight.
Worked example (reference example from the proposal):
f #[weight(100)] Nothing True = af #[weight(2000)] Nothing False = bf #[weight(50)] (Just _) True = cf #[weight(50)] (Just _) False = dOuter case: Nothing = 100 + 2000 = 2100, Just = 50 + 50 = 100. Inner
case under Nothing: True = 100, False = 2000. The rule generalizes
to any number of columns by repeated marginalization.
2.3 Wildcards, or-patterns, and guard fallthrough
- A wildcard in a single-column match is one alternative (
DEFAULT-like) and carries its clause weight directly; there is no distribution across constructors. - In multi-column matches a wildcard (or or-pattern, or guard fallthrough) clause is reachable from multiple constructor groups. Each decision point keeps locally coherent ratios (the marginalization rule of 2.2 applied to reachable right-hand sides), but the clause’s weight is then counted at more than one path, so clause-level ratios are not preserved globally. This is inherent: two clause weights constrain three or more execution paths. Downstream consumers (layout, dispatch, allocation) require only per-decision-point coherence.
- When the compiler detects this weight inflation (a clause’s effective
total exceeds its stated weight), it MUST report
incoherent-weights, informationally: the weights remain usable. - A weight on an unreachable clause (covered by earlier patterns) is ignored; the ordinary redundancy warning applies.
2.4 Transformation rules
Optimizations that restructure decision points MUST reconcile weight spaces as follows. These rules are the correctness core of the feature.
- Known scrutinee. When only one alternative survives, the decision point is eliminated and its weights are discarded.
- Decision-of-decision. When a decision on a value produced by another decision is pushed inward, the outer alternatives keep their weights and each duplicated inner decision keeps the original inner weights. This is correct under the independence assumption that the inner outcome does not depend on which outer alternative was taken; when later simplification collapses an inner decision, its weights are discarded.
- Dead-alternative elimination (flow sensitivity). When an
alternative becomes impossible inside a branch that already decided the
scrutinee, dropping it is conditioning on its negation. The surviving
weights MUST NOT be rescaled: relative weights already encode the
correct conditional ratios. (Example: alternatives
10 : 20 : 10with the first impossible leave20 : 10, which is exactly $P(B \mid x \neq A) = 2/3$.) - Decision merging. When two decisions on the same scrutinee merge
into one, the two relative weight spaces are reconciled: for each outer
alternative of weight $w_i$, the merged weight is $w_i \cdot S$ where
$S$ is the sum of the inner weights; for each inner alternative of
weight $u_j$ under the outer default $w_d$, the merged weight is
$w_d \cdot u_j$. The merged set is then divided by its greatest common
divisor. Worked example: outer
10 : 290(default 290) with inner200 : 100merges to10*300 : 290*200 : 290*100=3000 : 58000 : 29000, reduced to3 : 58 : 29; $P(B \mid x \neq A) = 58/87 = 2/3$ is preserved exactly. If either decision is only partially annotated (its weights were dropped by the all-or-nothing rule), the merge proceeds with no weights at all. - Rewrite rules. When a rewrite (see Modules and compilation specification section 5) replaces an expression, the target’s weights are replaced entirely by the template’s weights; there is no merging between them. A template without weights produces unweighted alternatives.
- Inlining. Weights are properties of alternatives, not of source functions: inlining preserves them (see section 5).
New decision points created by transformations (worker/wrapper, case splitting, and similar) carry no weights.
3 Branch weight wrappers
cold, likely, and unlikely are not separate compiler mechanisms.
They are wrappers that expand to manual weights, exactly as the reference
design expresses them as ordinary functions over weights:
| Wrapper | Expands to | Reading |
|---|---|---|
#[likely] |
#[weight(2000)] |
this alternative dominates |
#[unlikely] |
#[weight(1)] |
this alternative is rare |
#[cold] |
#[weight(0)] |
negligible; cold-section candidate |
The numbers are the reference design’s: 2000 and 1 are the conventional
likely pair (equivalently any ratio near 2000:1), and 0 is the cold
marker. Because the wrappers are weights, the all-or-nothing rule applies
to them uniformly: annotating one if branch #[likely] requires the
other branch to carry a weight or wrapper too (typically
#[unlikely]).
A Bool helper in the style of C/C++ likely() is likewise just a
function over weighted case; it is not a builtin:
likely :: Bool -> Boollikely c = case c of #[weight(2000)] True -> True #[weight(1)] False -> False3.1 Consumption
Weights flow to every consumer that can use frequency information: code
layout (hot paths as fall-through), switch dispatch (hoisting the
dominant alternative, weighted pivots), register allocation (frequency-
based spill choice), and LLVM branch_weights metadata on conditional
branches (!prof !{!"branch_weights", i32 w_true, i32 w_false}). Exact
lowering is a backend matter; the semantic contract is only that the
ratios of section 2 are what the backend receives.
3.2 Inference
The compiler MAY infer weights for a decision point that carries no explicit weights, using structure-based heuristics (a bottoming alternative is rare, a recursive call is frequent). Inference MUST NEVER combine with explicit weights: any explicit weight on a decision point governs all of its alternatives (all-or-nothing), and a decision point whose weights were dropped proceeds as if unannotated. Inference MUST NOT change program meaning (section 1.3).
3.3 Hot and cold splitting
An alternative whose weight is 0, and any alternative whose incoming
frequency falls below the backend’s cold threshold relative to its
function’s total, MAY be emitted into a cold section (__text_cold on
Mach-O, .text.unlikely on ELF) with the corresponding layout
reordering. The threshold is a target profile parameter (see Platform
proofs and SIMD specification
section 1).
4 Representation, layout, and alignment
Representation attributes are semantic attributes (section 1.3): they change the ABI of a type and belong in the interface contract projection.
4.1 Attributes
#[repr(C)]on adataorrecorddeclaration: the type has the platform C layout algorithm applied to its fields in source order (natural alignment, tail padding to the type’s alignment). This is the only representation that MAY appear at aforeign import/foreign exportboundary (see Foreign function interface specification section 2).#[repr(transparent)]on a single-fielddataorrecord: the type has exactly its field’s representation, size, and alignment (a zero-cost wrapper, valid at the FFI boundary).#[repr(align(N))]: the type’s alignment is at leastN.NMUST be a power of two. Raising alignment MUST NOT change field offsets.#[repr(packed)]or#[repr(packed(N))]: padding is removed; fields sit at offsets rounded down tomin(N, field alignment)withNdefaulting to 1.#[repr(u8|u16|u32|u64)]on anenum-likedata: the constructor discriminant has the given fixed-width representation.
Field order is source order (the layout table of Primitive types
specification section 9 is the ground
truth for the field types’ sizes and alignments). The compiler computes
offsets, size, and alignment by the C algorithm unless packed applies;
computing them any other way is a contract violation.
4.2 Safety rules
- A field whose offset is not a multiple of its natural alignment (as
with
packed) cannot be borrowed:Ref r aandMut r aintroduction on such a field MUST be rejected, since a reference asserts alignment. Raw access throughBytesor an FFI-provided pointer remains possible with the ordinary raw-memory obligations (platform specification section 2). #[repr(align(N))]withNbelow the type’s natural alignment is an error; the attribute only raises alignment.- Taking apart a
#[repr(transparent)]wrapper (single-field) is layout- preserving in both directions.
4.3 Computed layout facts
A type’s size, alignment, and field offsets are target-profile-indexed
facts. The compiler MUST record them in the compiled interface’s ABI
section (they are part of the contract projection per section 1.3), and
dependent specifications MAY compute with them once the dependent core
lands (the Usize width fact of primitive types section 2 is the same
kind of target-indexed fact). Changing a representation attribute of a
persisted type is a state-representation change: Runtime evolution
specification migration checking
applies.
5 Inline hints
#[inline], #[inline(always)], #[inline(never)], and #[noinline]
are hint attributes on function declarations (equations):
#[inline]: inlining is desirable at the optimizer’s discretion.#[inline(always)]: inlining is required unless impossible (an obligation on the optimizer, not on semantics).#[inline(never)]: the function must not be inlined.#[noinline]: an accepted spelling of#[inline(never)], matching LLVM and C/C++ attribute names. The two spellings carry the same hint and the same lowering; using both on one function is a duplicate attribute (section 1.3).
Inline hints MUST NOT change program meaning (section 1.3). They are not
part of the contract projection. When a function with inline hints and
branch weights is inlined, the weights travel with the inlined
alternatives (section 2.4), so a hint’s effect on layout composes with
the weights already attached to the code. The optimizer’s preservation
discipline (modules specification section 5, and the optimizer slice in
compiler/CONFORMANCE.md) treats all hints as erasable annotations.
A #[cold] MAY also appear on a function declaration, meaning the
function is unlikely to be called (its entry is a cold-section
candidate); this is the wrapper of section 3 applied to the function
entry rather than to an alternative.
6 Macro interaction
Attribute macros (modules specification section 8) MAY introduce, consume, rewrite, or validate attributes. The rules that keep this sound:
- A macro that emits a compiler attribute MUST emit only valid forms; the compiler validates the expanded result under ordinary rules (generated declarations are checked under ordinary rules, per the modules specification).
- Attribute provenance survives expansion in source maps (section 1.2).
- A macro’s own attribute namespace is hygienic: two macros’
#[...]names cannot collide silently.
7 Worked example
module Fast where
-- Layout: this type is the C ABI of a foreign record. #[repr(C)] record Header where #[repr(align(8))] magic :: U64 len :: U32
-- Branch weights: hot parse loop, cold error arm. classify :: U8 -> Nat classify b = case b of #[weight(2000)] 0 -> 0 #[weight(1)] _ -> 1
-- The wrappers, mixed with a manual weight (complete annotation). fastPath :: Bool -> Bool -> Nat fastPath a b = if #[likely] a then if #[weight(100)] b then 1 else 2 else 0
-- Inline hint with weights preserved through inlining. #[inline] saturating :: Nat -> Nat saturating n = case n of #[cold] 0 -> 0 #[weight(2000)] _ -> n8 Relationship to the reference design
The weight algebra (relativity, zero, all-or-nothing, marginalization, the transformation rules, and the constants 2000/1/0) follows GHC proposal 744 (Branch Prediction Hints) and its review discussion, which this project treats as the reference. Deliberate differences:
- The surface spelling is the
#[...]attribute system of this chapter, not a{-# WEIGHT n #-}pragma; the semantics above are unchanged. cold,likely, andunlikelyare wrappers over manual weights (section 3), matching the reference design’s conclusion that they are expressible as weight assignments rather than separate mechanisms.- Inference (section 3.2) is optional here rather than part of the core feature; the all-or-nothing rule is the same.
9 Open decisions
- Warning vs error for partial annotation (
incomplete-branch-weights) and for weight inflation (incoherent-weights): the reference design warns and drops or keeps; this draft currently inherits that. A stricter profile that errors is a possible target-profile choice. - Whether
#[repr(align(N))]on fields (rather than whole types) is admitted; this draft allows it on types and on fields ofrepr(C)records with the constraint of section 4.2. - Whether explicit numeric weights and wrappers may be mixed on one decision point (this draft allows it: wrappers ARE weights) and whether a warning should normalize surprising ratios.
- The cold-splitting threshold’s exact default and its gating behind optimization levels (platform target profile parameter).
- Whether weight-0 alternatives may be excluded from coverage checking’s exhaustiveness reasoning (this draft says no: coverage is semantic and weights are hints).
- Whether inline hints should have any effect on interface identity for cross-module inlining decisions (this draft says no: identity is contract-only per D9).
10 Safety assertion attributes
These attributes are compiler-owned and verified at the definition;
a violation is a compile error naming the attribute, the asserted rule,
and the offending site. A satisfied assertion is a contract fact: it is
recorded in the module’s interface and hashed into iface_hash exactly
as repr facts are (section 4.3), so consumers may rely on it.
Attribute names may be hyphenated and arguments are parenthesized
(#[recursion("tail")], consistent with #[weight(n)]); the let _ = e binder is the documented form of an explicit discard.
| Attribute | Asserts |
|---|---|
#[recursion "none"] |
the definition makes no recursive calls (self or mutual) |
#[recursion "tail"] |
all recursive calls are tail calls in D104’s sense (constant stack) |
#[recursion "bounded(n)"] |
call depth is statically at most n: structural recursion over Vec/Nat sizes proves it via the totality machinery; otherwise a conservative call-graph bound applies and the report says which |
#[stack "constant"|"bounded(n)"] |
stack usage bound; "constant" implies tail or non-recursive |
#[no-alloc] |
the body contains no allocation sites (cons, closures, thunks, Mut, strings, Bytes, foreign aggregates) |
#[alloc "once"] |
at most one allocation site runs per call (initialization pattern) |
#[max-complexity n] |
cyclomatic complexity (branches + 1 over the body) at most n |
#[max-lines n] |
body length at most n source lines |
#[assert-min n] |
at least n explicit runtime assert/decide-on-runtime-value checks in the body (proof obligations do not count) |
#[checked-params] |
every parameter is validated before first use |
#[no-macros] |
no macro expansion occurred within the definition (checked from expansion provenance) |
#[no-raw-ptr] |
no RawPtr/opaque in the signature and no foreign pointer operations in the body |
#[deny-warnings] |
warnings in scope are errors (warn # error scoped to a module or definition) |
Discarding a Result-typed expression without ignore is a check
error (rule 7’s return-value discipline as a language default; the
must-use interface rows record it).
The jpl profile (#[profile "jpl"] on a module, or the jpl target
profile for a program) makes the whole set mandatory at the JPL rule
numbers (complexity 10, lines 60, assert-min 2 on exported
functions, recursion bounded, no-alloc outside initialization, no
macros, no raw pointers, deny-warnings) and reports a whole-program
summary. A definition’s own attributes may be stricter than the
profile, never looser.
The rules this vocabulary cannot verify (bounds on non-structural
recursion) are refused or conservatively bounded with the report
saying so, never silently trusted. The full rule-by-rule mapping to
the JPL “Power of 10” is design/safety-assertion-attributes.md.
11 Implementation status
Implemented (P17 and P20): the attribute system (attachment points,
compiler vs macro ownership, duplicate handling), weights with
marginalization and the 3:58:29 case rule, inline hints, repr
layouts with abi.layouts hashed into iface_hash, and the safety
assertions of section 10 with their interface facts. Evidence:
compiler/tests/attributes.rs and the safety suite. The design
context for the AST representation of attributes is
design/attribute-annotations.md; the JPL rule mapping is
design/safety-assertion-attributes.md.
Amendments
Dated notes recording landed decisions (compiler/CONFORMANCE.md,
D-numbers) that amend or interpret this chapter’s normative text.
2026-10-28, P15 conformance sweep.
- Weights, placement, and reports (D49-D54), 2026-10-28. This
amends sections 1-2: the
ifcondition position is a pair form (if #[w] c then t else eweights the true branchwand completes the pair with reference base 1, soif #[likely] cis the conventional 2000:1 pair); attributes beforethen/elseannotate their branch directly under the one-branch rule, an explicit else-side weight overrides the pair’s implicit 1, and annotating the true side twice is an error (D49). A guard’s weight annotates its pass outcome with fallthrough completed by the base 1 (D50). Type-position attributes parse at record field types and merge with the field-declaration position (D51).reprscope:repr(C)/align/packedreject on multi-constructordata(repr(uN)covers enums),transparentis exclusive, conflicts reject, and a fixed-discriminantdatarecords the discriminant as an interface fact keeping the uniform-word representation (D52). Duplicates are errors except macro-owned names; one weight-family attribute per alternative, one inline hint per function, one#[cold](D53).incomplete-branch-weightsis a warn-and-drop warning andincoherent-weightsinformational (weights kept), both non-fatal (D54). - Transformation rules (D61), 2026-10-28. This amends section 2.4:
over compiled shapes (default alternatives expanded per constructor
with scrutinee synonym
lets) inner alternatives outside the default set are conditioned away unrescaled, then the rescale formula and GCD run; dead-alternative removal is negation conditioning; case-push duplicates decisions keeping weights. Evidence:tests/optimizer.rs::weights_survive_optimization. - Safety assertions (D111-D114, D176-D186), 2026-10-28. This
amends section 10: the vocabulary is landed with hyphenated names and
parenthesized arguments (
#[recursion("tail")]) and thelet _ = ediscard form; assertions verify at the definition (violations name rule and site) and satisfied assertions are contract facts (assertions/must_userows hashed intoiface_hashexactly likerepr);bounded(n)proves structural depth first (decrease_boundsize-index detection), falls back to the literal ladder with the report saying which, and refuses what it cannot bound (numeric-over-structural claims are deliberately refused and pinned as negatives); thejplprofile exists as module attribute andTargetProfile::jpl()with a whole-program summary, per-definition attributes stricter only, and discarding aResultwithoutignoreis a check error. Evidence:tests/safety.rs. - LLVM contract attributes (D187-D191), 2026-10-28. Beyond
section 5’s erasable hints, code generation emits effect-derived
function attributes (
memory(none) speculatable,memory(read/readwrite),nofree, proven-onlywillreturn) and usage-derived parameter attributes (captures(none)/nonnull/noundef/align,!range,!align) as presence/absence pairs, each attribute present where proven and absent where unproven;llvm.assumecarries operand bundles from assertions and repr facts (norangebundle exists in this LLVM; ranges use the return attribute and!rangemetadata);freezeguards every untrusted ingress and all fault paths are audited (noreturn/unreachable). Evidence:tests/llvm_contract.rs. - Cinch naming (D278-D281), 2026-10-28. The language is Cinch:
sources
.cinch, interfaces.cinchif, runtime externscinch_*; artifact names in this chapter read with those names.
