5. Declarations and scope
5.1 Local initialization
CS-DECL-001. let introduces a new local and always requires an initializer. A type annotation does not supply an implicit initializer. The compiler must not insert nothing or default when the initializer is omitted.
let count = 0
let customer as Customer = nothing
let options as Options = default
Invalid:
let count
let customer as Customer
let value = default // invalid without a determinable target type
Target-typed default resolution belongs in chapter 4. Lazy initialization retains the declaration's type while deferring its initializer, as defined in chapter 18.
5.2 Readonly bindings
CS-DECL-002. let readonly initializes a local once and prohibits later reassignment, compound assignment, increment/decrement, passing the local through a writable reference parameter.
CS-DECL-003. The readonly restriction applies to the binding, not to the transitive object graph. Mutating a writable member of the referenced object remains valid. Readonly does not imply freezing, copying or thread safety.
let readonly customer = LoadCustomer(id)
customer.Name = "Ana" // valid if Name is writable
customer = otherCustomer // invalid
A runtime-computed readonly value is distinct from const, whose value must satisfy CLR-compatible constant restrictions. Readonly binding does not imply deep immutability.
5.3 Name scope and iteration bindings
CS-DECL-004. A local or parameter must not be shadowed by another local/parameter in a nested scope. Name comparison is case-insensitive. Ordinary member/parameter naming remains possible through member qualification.
CS-DECL-005. Iteration variables are readonly bindings. They cannot be reassigned, incremented, passed as writable byref, or used as writable swap targets. Referenced mutable objects remain mutable.
for_each customer in customers
customer.Name = "Ana"
customer = otherCustomer // invalid
end for_each
Special capture scopes and explicit let outer promotion require their own scope rules; they do not grant implicit shadowing.
5.4 Object extraction
CS-DECL-006. let from ... end let declares named locals by extracting from a source object. Each source-relative access requires leading .. Types are inferred unless written explicitly. Each declaration includes its local name; a bare .Name is not a declaration.
The header accepts an ordinary source expression, including an action/method call returning the source object. Evaluate it once before extracting values; every contextual leading-dot access in the block uses that captured object. Apply normal expression binding, typing and async-context rules. Declarations evaluate their right sides in written order. Header evaluation failure prevents extraction, and evaluated getter/call exceptions propagate normally. The construct does not copy or dispose its source.
The captured source is required. If it is nothing, fail under required-source access rules before evaluating any extraction right side. Do not skip the declarations, populate absent results, or substitute a default object in strict let from. This presence check concerns the source object; a property read from a present source can itself return nothing under its normal contract.
let from customer
name = .Name
email as String = .Email
end let
A source call can be written directly:
let from LoadCustomer(id)
name = .Name
email as String = .Email
end let
Reference C# lowering captures the application result once and checks presence before reading its members:
var __source = LoadCustomer(id);
if (__source is null)
throw new System.NullReferenceException("Extraction source is missing: LoadCustomer(id)");
var name = __source.Name;
string email = __source.Email;
LoadCustomer runs once; Name is read before Email. The two reads use the same captured object, rather than making separate calls that could return different customers.
Conditional extraction
CS-DECL-009. let_if from ... end let_if evaluates a source expression once. If the captured object is present, evaluate the named extraction expressions in written order against that captured object. If it is nothing, initialize every declared local to nothing without evaluating any extraction right side, getter, call or other dependent expression. The locals remain declared under the same scope and collision rules as ordinary extraction; the missing branch assigns absence rather than omitting the declarations.
let_if from LoadCustomer(id)
name = .Name
email as String = .Email
end let_if
The application supplies a Customer with String Name and Email members. If no customer is returned, both locals hold nothing, not empty strings. Header evaluation exceptions propagate rather than assigning absence; getter/call exceptions from a present source also propagate. Conditional extraction does not copy, construct, repair or dispose a source.
Reference C# for these reference-type locals:
var __source = LoadCustomer(id);
string? name;
string? email;
if (__source is null)
{
name = null;
email = null;
}
else
{
name = __source.Name;
email = __source.Email;
}
The captured source is tested before any member access. Every local is assigned in both branches, so subsequent code can check for absence explicitly. Compile-time member binding and expression checks still apply to extraction expressions even when a runtime source is missing.
Conditional extraction locals have absence-capable types fixed at declaration. A non-nullable CLR value type T uses System.Nullable<T>, for inferred locals and explicit as T declarations alike. An already-nullable value type remains that type rather than being nested. Reference types preserve actual nothing. This rule is specific to conditional extraction; it does not change ordinary type defaults or silently replace absence with zero, false, empty strings or constructed values.
For an application Customer with Integer Age and Decimal CreditLimit members:
let_if from LoadCustomer(id)
age as Integer = .Age
creditLimit = .CreditLimit
end let_if
Age has Nullable <of Integer> type and creditLimit has Nullable <of Decimal> type, including when the source is present. Reference C#:
var __source = LoadCustomer(id);
int? age;
decimal? creditLimit;
if (__source is null)
{
age = null;
creditLimit = null;
}
else
{
age = __source.Age;
creditLimit = __source.CreditLimit;
}
Apply normal assignment, generic and CLR type restrictions to the absence-capable target. If the declaration's type cannot support an appropriate CLR absence representation, report a compile-time error. Do not silently box the local into Object or introduce a custom optional wrapper. Subsequent calls to APIs requiring non-nullable values must use the normal presence/unwrapping or explicit fallback rules.
5.5 Diagnostics and reference C# lowering
Reject missing initializers, case-insensitive local/parameter shadowing, and writes through readonly bindings. No source-language tuple destructuring is introduced.
A readonly local can lower to an ordinary C# local:
var customer = LoadCustomer(id);
The DirectSharp compiler enforces the readonly rule before lowering. A runtime holder or a fabricated C# local modifier is unnecessary.
5.6 Multiple declarations and outer scope
CS-DECL-007. Several named locals may be initialized in one let declaration. Each binds to its corresponding expression and infers its own type; arity must match except for the explicit shared-type/default form below. This is positional assignment, not tuple destructuring. Right-side values are obtained before their assignment to the newly declared locals.
let quantity, description = 3, "Printer paper"
let subtotal as Decimal, tax as Decimal = 25, 5
let first, second as String = default
The shared explicit type/default form initializes each named local with that type's default. It does not generalize a single effectful initializer into unspecified repeated execution.
CS-DECL-008. let outer declares into the immediately enclosing lexical scope while the initializer still executes at its authored statement. Promoted storage is initialized to nothing before the inner block; the initializer stays at its original statement. After a skipped branch the value remains nothing, so readers must account for absence. Promotion climbs exactly one lexical scope. Loop iterations reuse the same promoted storage, leaving the last successfully assigned value or nothing. In a try body, the target is the scope enclosing the whole try, visible to sibling catch/finally bodies. Name-collision rules still apply.
5.7 Compile-time constants
CS-DECL-010. const declares a genuine CLR-compatible compile-time constant,
with an initializer and an optional explicit type. Inference must determine a
legal constant type. Ordinary numeric, Boolean, Char, String and enum constants,
and compatible nothing values, follow the target's constant
restrictions. Constant expressions and references to compatible constants are
permitted; runtime calls, object creation and values such as DateTime.UtcNow
or Guid.NewGuid() require ordinary runtime storage. Constants cannot be
reassigned or passed as writable reference/output locations. DirectSharp name
identity and operator semantics still apply.
public class RetryPolicy
const MaxRetries as Integer = 3
public constants
ProductName = "DirectSharp"
InitialDelay = 2
end constants
action Process()
const attempts = MaxRetries
// Use attempts in ordinary application logic.
end action
end class
A type constant is inherently associated with its declaring type and has ordinary
member visibility; it is distinct from a runtime static readonly member.
constants ... end constants groups are type-level declarations. Local constants
use individual const declarations, with ordinary local scope and collision
rules; local constant groups are not supported. CLR public constants can be
embedded into consuming assemblies: use runtime readonly storage for public
values intended to change independently of consumer recompilation.
Specification contents · Previous: 4. Types · Next: 6. Expressions and operators