v0.22.0—Bounded nullability checks and practical .NET migration guidance.See what's new

Documentation

Bindings

The §B tag introduces a local binding (Calor's equivalent of a C# var declaration). The binder accepts four forms; in three of them the type is inferred from the initializer expression.

Code blocks here are fragments inside a module or function. Complete checked nullable-reference examples are in the nullability guide.

Reference for RFC v0.6-bind-inference-formalization. The behavior on this page is what the binder does today and is pinned by tests/Calor.Semantics.Tests/BindInferenceDocsTests.cs.


Syntax

Calor
§B{name}                    // (1) requires an initializer (see Rules)
§B{name} initializer        // (2) immutable, type inferred from initializer
§B{name:type}               // (3) immutable, declared type, no initializer
§B{name:type} initializer   // (4) immutable, declared type wins

§B{~name} initializer       // (2') mutable variant of (2)
§B{~name:type}              // (3') mutable variant of (3)
§B{~name:type} initializer  // (4') mutable variant of (4)

The ~ prefix marks the binding as mutable; without ~ the binding is immutable (cannot be reassigned).


Rules

  1. Either a :type annotation or an initializer is required. §B{x} with neither is a hard error (Calor0250, diagnostics below).
  2. An explicit :type wins over inference. Form (4) does not re-infer; the declared type supplies the receiving boundary. The Calor 0.22 compiler rejects supported possibly-null initializers for non-null destinations with Calor0272. Ordinary type checking also checks type compatibility; an annotation does not make an incompatible value valid.
  3. Without :type, the bound type is initializer.TypeName. This is "shallow" inference: the binder reads the type the initializer's bound node reports and uses it directly. There is no bidirectional flow, no widening, no constraint solving.
  4. The reported type is the binder's internal name. Literal initializers produce INT, STRING, BOOL, FLOAT. Calling code that needs to write the equivalent type annotation uses the user-facing names instead — see Types for the mapping (i32, str, bool, f64, …).
  5. Inferred bindings emit var in C#; explicit bindings emit the named type. Writing §B{x} INT:0 produces var x = 0;; writing §B{x:i32} INT:0 produces int x = 0;. Both compile to the same IL, but the source-level distinction is preserved end-to-end.

Inference table

For each initializer shape on the left, the resulting bound type when no :type annotation is present is on the right.

InitializerBound typeNotes
INT:42 (or 42)INTBoundIntLiteral. Values outside the 32-bit range are bound as LONG.
STR:"hello"STRINGBoundStringLiteral
BOOL:true / BOOL:falseBOOLBoundBoolLiteral
FLOAT:3.14FLOATBoundFloatLiteral
(+ a b) etc.result type of the binary opBoundBinaryExpression.TypeName — type of the result, not the operands
an identifier xdeclared type of xthrough BoundVariableExpression
§C{Foo.bar} … §/Cdeclared return type of Foo.barthe binder uses the call's bound return type
null or untyped §NNinference fails by defaultCalor0251; provide an appropriate explicit nullable or Option type

The literal-type names on the right (INT, STRING, …) are the binder's internal type identifiers. They appear in tool output and diagnostics. The corresponding type annotation you would write in source is the lowercase user-facing name (i32, str, bool, f64).


Examples

Inferred, immutable

Calor
§B{score} INT:85               // score: i32 = 85
§B{name} STR:"Calor"           // name: str = "Calor"
§B{active} BOOL:true           // active: bool = true
§B{pi} FLOAT:3.14              // pi: f64 = 3.14

Inferred, mutable

Calor
§B{~counter} INT:0             // mutable, starts at 0
§B{~total} INT:0
§L{i:1:10:1}
  §SET counter (+ counter INT:1)
  §SET total   (+ total i)

Explicit type, no initializer

Calor
§B{retries:i32}                // declared but uninitialized — initialise before use
§B{user:str}

These forms are typically used where a value is assigned in every branch of a subsequent §IF / §W, or where the binding is for a field initialized in a constructor body.

Explicit type with initializer

When you want the declared type to drive the conversion of the initializer (rather than be inferred from it), give an annotation:

Calor
§B{count:i64} INT:42           // i64 binding initialised from an i32 literal
§B{x:f64}     INT:0            // f64 binding initialised from an integer literal

Inferred from a call expression

Calor
§B{user} §C{repo.FindUser} §A id §/C
// user: User (declared return type of repo.FindUser)

Inferred from a binary op

Calor
§B{sum} (+ a b)                // sum: result type of (+ a b)
§B{half} (/ x FLOAT:2.0)       // half: f64

Diagnostics

Calor0272 — possibly-null initialization

Calor 0.22 rejects a possibly-null initializer at a supported non-null receiving boundary. Inference retains supported initializer nullability: omitting :str does not make a nullable call result non-null. Initializing a mutable binding is checked, but later assignment/rebinding is outside this guarantee. Constructor results and constructor inputs are separate: the latter are not covered. See Nullability and .NET Interop for the support table and complete rejected/default/throw/pattern examples. Calor0273 checks native returns and Calor0274 checks method inputs. The strict-inference opt-out below does not disable these binding errors.

Calor0250 — BindRequiresTypeOrInitializer

A §B{name} declaration must carry either a :type annotation or an initializer expression. With neither, the binder cannot choose a type for the new symbol.

Calor
§F{f001:Foo:pub} () -> i32
  §B{x}            // Calor0250: §B{x} requires :type or initializer
  §R INT:0

Fix by adding one or the other:

Calor
§B{x:i32}          // explicit type
§B{x} INT:0        // initializer (inferred)

Pre-v0.6 behavior: the binder silently defaulted to INT in this case, producing wrong-typed code with no diagnostic. The diagnostic was added in v0.6 and is enforced through the root calor -i compile pipeline by BindValidationPass.

Strict-mode diagnostics (default-on since v0.6.3)

The following diagnostics are reserved in the Calor0250-0259 range and are enforced by default as of v0.6.3 (RFC §6). To opt out during migration, pass --no-strict-bind-inference to the root compile command or set CompilationOptions.StrictBindInference = false.

CodeTitleFires on
Calor0251BindCannotInferNullLiteral§B{x} §NN (untyped None) or §B{x} null
Calor0252BindCannotInferGenericReturn§B{x} §C{Vec.empty} §/C and other well-known generic factory targets (Vec.empty, List.empty, Array.empty, Set.empty, Map.empty, …)
Calor0253BindAmbiguousNumeric§B{x} (+ INT:0 FLOAT:0.0) — a binary op mixing integer and floating-point literal operands

Each fires only when the binding lacks an explicit :type annotation; adding the annotation always silences the diagnostic.

Calor
§B{x:Option<i32>} §NN              // silences Calor0251
§B{x:Vec<i32>} §C{Vec.empty} §/C   // silences Calor0252
§B{x:f64} (+ INT:0 FLOAT:0.0)      // silences Calor0253

Round-trip

Both forms round-trip stably through calor convert and calor format:

  • A C# var x = 42; becomes §B{x} INT:42 (inferred form).
  • A C# int x = 42; becomes §B{x:i32} INT:42 (explicit form).
  • Going back, §B{x} INT:42 becomes var x = 42;; §B{x:i32} INT:42 becomes int x = 42;.

The emitter never adds an annotation that the source did not already have, and the binder never drops an annotation that the source did have.


See also

  • Types — primitive type names and their C# equivalents
  • Structure Tags — §F, §I, §O and the surrounding function structure
  • Expressions — initializer expressions in Lisp-prefix form
  • Calls — §C calls as binding initializers