On this page

6. Expressions and operators

6.1 Evaluation and operator binding

CS-EXPR-001. Operands evaluate left-to-right, exactly once, subject to short-circuiting. Precedence determines grouping, not permission to duplicate or reorder side effects. Parentheses override grouping; user-defined operators cannot change precedence.

CS-EXPR-002. and, or and ?? short-circuit. The right operand of and runs only when needed after a true left operand; the right operand of or only after a false left operand; the fallback of ?? only when the left operand is actual nothing.

CS-EXPR-003. = means equality in conditions/expressions and assignment in an allowed assignment-statement context. Assignment must not become an accidental Boolean condition. ?? associates right-to-left; ordinary binary arithmetic associates left-to-right.

The source inventory includes arithmetic +, -, *, /, div, mod; equality =, <>, eq, not_eq; relational <, <=, >, >=; bitwise &, |, ^, ~; and supported shifts <<, >>, >>>. Full admissible operand types and generated equality/ordering contracts still belong in chapters 4/9 and appendix D. Operator existence does not imply that every CLR type supports it.

6.2 Arithmetic, division and remainder

CS-ARITH-001. / performs fractional division. Two integer operands are promoted before division to produce Double. A Decimal operand retains Decimal arithmetic when the other operand is compatible; existing floating-point operands use the appropriate ordinary floating-point type. The destination type must not silently change the calculation.

CS-ARITH-002. div truncates the quotient toward zero, not the operands individually, and produces an integral result, including for fractional or compatible mixed-type operands. A truncated Decimal, Double or Single value is not the result type of fractional div. Operand compatibility still applies; this rule does not make otherwise incompatible numeric types compatible. mod produces the ordinary .NET-like remainder, not Euclidean modulo. Multiplication remains *; no mul operator is introduced, and normal source uses mod rather than %.

Expression Value Reason
3 / 2 1.5 Fractional integer-operand division
7 div 2 3 Whole-number quotient
3.5 div 2 1 Truncate the fractional quotient
-3.5 div 2 -1 Toward zero, not floor
3.5 mod 2 1.5 Remainder from the original operands

CS-ARITH-003. *, /, div and mod share multiplicative precedence above binary + and -.

CS-ARITH-004. Supported authored arithmetic substitutes each missing operand's effective default locally to that operation, without changing stored values. An absent divisor is substituted before the underlying division/remainder operation. No special missing-divisor exception replaces normal Decimal, integral or floating-point behavior.

Integer-operand / follows the promoted Double operation. Its floating-point zero-divisor behavior must not be replaced by integer divide-by-zero behavior.

CS-ARITH-005. Imported user-defined CLR operators retain their own null and exception contracts. The compiler must not synthesize an external custom object's default or rewrite a direct imported call merely because DirectSharp has authored default-substitution rules.

CS-ARITH-006. For fractional or compatible mixed integral/fractional operands, div produces Long (System.Int64). Calculate the quotient in the operands' compatible numeric type, then convert the quotient to Long using ordinary CLR conversion semantics. Do not cast an operand to Long before dividing. Existing integral-only promotion and division rules are unchanged.

Applicable checked/unchecked contexts and generated-project settings govern the result conversion as they do an ordinary C# cast. No additional always-checked conversion is introduced. Decimal-to-Long conversion throws OverflowException when out of range even under unchecked. Checked floating-point-to-Long conversion rejects out-of-range, NaN and infinite values; unchecked conversion follows ordinary CLR behavior without a promised portable result for those values. Constant diagnostics follow the applicable CLR conversion rules. Division itself retains its numeric type's behavior before conversion.

Rounding is separate from div. Round, Truncate, Floor and Ceiling are library functions rather than extra operators; DirectSharp Round uses AwayFromZero as its default midpoint choice.

6.3 Boolean negation and carried subjects

CS-EXPR-004. not binds below equality, relational comparisons, is predicates and binary bitwise &, ^ and |, but above Boolean and and or. This does not change numeric complement ~ or permit bitwise operands whose types do not support the operation.

CS-EXPR-010. Every is/is not predicate family binds at the relational-comparison tier, above equality. This includes type, identity, membership, absence/state, String classification/relationship, wildcard/sequence pattern and regex predicates. Negated forms have the same binding tier as their positive forms; the not within is not is not the separate low-binding Boolean not operator. Normal calls and member access retain primary-expression binding. Parentheses select explicit grouping, and grouping does not change once-only operand evaluation.

let expected = true
let text = "123"
let matches = expected = text is match "^[0-9]+$"
// expected = (text is match "^[0-9]+$"); matches is true
not age >= 18       // not (age >= 18)
not a = b           // not (a = b)
not a & b           // not (a & b), for Boolean a and b
not a and b         // (not a) and b

CS-EXPR-005. A comparison/predicate may establish a subject reused by subsequent predicate terms in the same and group. Evaluate that subject once; a leading dot selects its members. or starts a separate alternative and does not silently inherit the previous subject. Parentheses delimit groups.

if GetCustomer(id) is not nothing
    and .IsActive
    and .Balance > 0 then
    Process()
end if

if age >= 18 and <= 65 then
    Approve()
end if

For a = b and is not nothing, the carried subject is a, not b or the first comparison's Boolean result. Later terms retain short-circuit evaluation. General mathematical chaining such as 18 <= age <= 65 is not part of the grammar.

Chapter 16 defines scalar/collection emptiness and string comparison modes. Boolean conditions do not acquire C#-like arbitrary truth conversion.

6.4 Conditional expressions

CS-EXPR-006. The conditional family is if(condition, whenTrue, whenFalse), if(condition, whenTrue) and ifnot(condition, whenFalse). Evaluate the condition and only the selected result expression. The one-branch forms produce actual nothing when their branch is not selected, not the result type's default.

let warning = if(order.IsLate, "Order is late")
let fallback = ifnot(found, LoadFallback())
let caption = if(enabled, "Enabled", "Disabled")

CS-EXPR-007. Conditional results require compatible contextual/inferred typing. One-branch forms must represent their absent result through the compatible optional/nullable result type. Two-argument if is not null coalescing, and C-style ?: is not accepted.

The unselected expression cannot throw or perform an effect because it is not evaluated. Exceptions from the condition or selected result propagate normally.

6.5 Presence fallback and explicit failure

CS-EXPR-008. value ?? fallback preserves a present value, including zero, false or empty String/collection, and evaluates the fallback only on actual absence. The result must satisfy the ordinary type rules. value ?? nothing preserves absence without installing later arithmetic-propagation metadata.

classValue ?? default follows the normal effective-default resolution. For a class with no visible intrinsic/external custom default, that default is nothing, so the result may remain absent. The expression is legal; it does not implicitly construct an instance or require a custom default declaration. When a visible valid custom default exists, evaluate it only if classValue is missing, preserving ordinary visibility, locking, uniqueness and compatibility rules.

let customer = existingCustomer ?? default

For an existing Customer reference whose type has no custom default, reference C# is:

Customer? customer = existingCustomer ?? (Customer?)null;

A present reference is preserved; an absent one stays null. Choosing the effective default does not promise a present result, mutate existingCustomer or add a future assignment policy. A targetless bare default remains an inference error.

CS-EXPR-009. value ?? throw checks presence immediately. For a direct action parameter the default failure is ArgumentNullException with its actual parameter name; for an ordinary variable, property chain or expression it is NullReferenceException. A custom message preserves this category; explicit exception construction uses the chosen type.

Using ?? throw in a local initializer adds no persistent required-value invariant to that local. It checks only when the expression executes. Later assignments obey ordinary type and writability rules; they do not acquire an implicit presence check from the initializer. A later explicit ?? throw is a fresh check. A non-nullable value type may still reject nothing on normal type grounds, and readonly bindings still prohibit reassignment.

let customer = LoadCustomer(id) ?? throw
customer = nothing

The application supplies LoadCustomer returning a Customer reference. The first line fails if that call returns nothing. The second is an ordinary mutable reference assignment and does not rerun the initializer's check. Reference C#:

Customer? customer = LoadCustomer(id)
    ?? throw new System.NullReferenceException("Required value is missing: LoadCustomer(id)");
customer = null;

Compiler-authored diagnostics retain a searchable source-expression suffix, avoid runtime values, and do not reevaluate the checked source. This presence-expression policy differs from state-contract failures in chapter 12.

let customer = repository.Find(id) ?? throw

6.6 Null-coalescing assignment

CS-ASSIGN-001. target ??= expression evaluates and assigns the right side only when the target's current value is nothing. It requires a writable target capable of representing absence, subject to the map-specific missing-key case in chapter 15. Empty String, zero, false, default-valued structs and existing empty collections do not trigger it.

CS-ASSIGN-002. Receiver/index expressions for a compound target must not be duplicated. The right side is lazy; ordinary accessor and assignment failures propagate. This construct does not imply atomicity.

let name = ""
name ??= "Unknown" // unchanged
GetCustomer().Address ??= CreateAddress()

Reject a readonly target or a definitely nonnullable value target to which the ordinary absence test cannot apply. Map missing-key insertion is an explicit exception to strict lookup, defined in chapter 15; it must not be implemented by an ordinary throwing map read followed by generic null testing.

6.7 Chained assignment and mutation boundaries

CS-ASSIGN-003. Chained assignment is allowed in assignment-statement context and associates right-to-left. Every target must be a simple writable local or parameter. Fields, properties and indexed locations are not valid chain targets, even when writable in an ordinary single-target assignment. Evaluate the final right-hand expression once, assign to the rightmost target, then assign that value to preceding targets. Normal assignment compatibility and readonly restrictions apply to every target.

a = b = Load()

CS-ASSIGN-004. General multiple assignment such as x, y = y, x and general discard assignment _ = Calculate() are not accepted. A value-returning action may simply be called as a statement when its result is unused. Tuple destructuring is also excluded.

CS-ASSIGN-005. The ordinary helper swap(a, b) exchanges the values of two writable locations of the same effective type. Resolve each location expression once, in written order, then exchange the values. Literal values, readonly bindings and iteration bindings are not writable targets. The exchange is generic over the value type; native Interlocked support for that type is not required. Normal CLR type/storage restrictions still apply. It does not introduce general multiple assignment. Its atomic form and concurrent-context exception are defined in chapter 17.

let first = 10
let second = 20
swap(first, second)
// first is 20; second is 10

For these local Integer bindings, an illustrative C# lowering is:

int first = 10;
int second = 20;
int temporary = first;
first = second;
second = temporary;

This temporary-variable lowering illustrates an ordinary exchange. It establishes neither an exported CLR helper signature nor synchronization for concurrent access.

Ordinary supported compounds include +=, -=, *=, /=, mod= and compatible bitwise/shift compounds. They use the corresponding valid operation and require an assignable result; user operators do not imply mathematically unrelated operators. No div= rule is inferred merely from div existing.

6.8 Checked and unchecked overflow contexts

CS-OVERFLOW-001. Outside an explicit context, normal C#/.NET and generated-project overflow settings apply. DirectSharp does not silently make all arithmetic checked.

CS-OVERFLOW-002. checked ... end checked and checked(expression) apply ordinary C# checked semantics to applicable integral arithmetic/conversions; overflow raises the normal OverflowException. Corresponding unchecked forms use ordinary unchecked semantics without promising wraparound for every type or operation. The innermost explicit context controls applicable source expressions.

checked
    let next = counter + 1
    let wrapped = unchecked(counter + 1)
end checked

CS-OVERFLOW-003. These contexts do not suppress unrelated exceptions, change default substitution, or retroactively change arithmetic inside an already-compiled external method. Constant diagnostics follow applicable checked/unchecked rules. The block supplies an ordinary lexical scope, not a transaction or error handler.

6.9 Diagnostics and reference C# lowering

Reject incompatible operator operands, invalid assignment targets, unsupported symbolic aliases and malformed conditional forms. A chain containing a field, property or indexed target is a compile-time error; for example, neither customer.Name = otherCustomer.Name = "Ana" nor items[0] = items[1] = 0 is a valid chain. Diagnose mismatched effective types or non-writable targets in swap. A runtime arithmetic/accessor exception is not a compiler diagnostic; preserve the chosen operation's ordinary failure behavior.

For two present Integer operands, fractional division must promote before the operation:

let ratio = 3 / 2
double ratio = (double)3 / (double)2;

For nullable Integer operands, a possible value-only lowering is:

int? left = ReadLeft();
int? right = ReadRight();
double ratio = (double)(left ?? 0) / (double)(right ?? 0);

Calls execute once in order. Dividing by substituted zero follows Double behavior; it is not the integer operation from the superseded example.

For compatible fractional operands, the result conversion surrounds the quotient:

let numerator = 3.9
let denominator = 2
let whole = numerator div denominator
decimal numerator = 3.9m;
int denominator = 2;
long whole = (long)(numerator / denominator);

The quotient is calculated as Decimal and converted to Long, producing 1. Casting numerator first would change the operation. With Double operands, (long)numerator / denominator can still produce a fractional Double result; (long)(numerator / denominator) produces the required integral result. Apply the source's effective checked/unchecked context to the applicable conversion; the destination must not alter calculation type.

For known Boolean and String types, a one-branch conditional can lower to:

string? warning = order.IsLate ? "Order is late" : null;

For a known Customer reference with writable Address property:

var receiver = GetCustomer();
var current = receiver.Address;
if (current is null)
    receiver.Address = CreateAddress();

6.10 Numeric helper member types

CS-MATH-001. The ordinary numeric library helpers have the twelve Decimal/Double member fragments in the API catalogue: Round(value), Round(value, Integer digits), Round(value, Integer digits, MidpointRounding mode), Truncate(value), Floor(value) and Ceiling(value) for each type. Return the same numeric type, not Long. Round(value) uses zero digits and Round without an explicit mode uses AwayFromZero. Explicit mode delegates to the corresponding Math.Round overload. Preserve native argument/failure behavior, including Decimal digits 0–28 and Double digits 0–15, and native typed Math behavior for Double NaN/infinities and binary representation. No additional Integer/Long/Single/nullable overload or conversion priority is selected; ordinary widening/binding applies and equally applicable Decimal/Double candidates can require an explicitly typed argument. Nullable method arguments do not acquire an arithmetic-default substitution rule. CS-MATH-002 specifies the export container and source-binding boundary. Imported Math calls keep their native defaults.

CS-MATH-002. The root public static class DirectSharp.Numbers contains CS-MATH-001's twelve members. Qualified Numbers.Round/Truncate/Floor/Ceiling access uses ordinary type/member lookup, with the normal automatic or explicit root namespace import. This does not establish unqualified member lookup: a namespace import is not an implicit static import. Preserve the recorded bare source forms for the compiler binding account; no qualified-only replacement, static-import grammar or hidden collision/overload priority is selected by the export declaration. Package/assembly policy remains separate.

6.11 Property reads and required operations

CS-ACCESS-001. Ordinary property/field read chains automatically propagate nothing. If the root or an intermediate receiver is absent, the read result is nothing and later members are not evaluated. Evaluate reached receiver segments once in source order. A getter that executes and throws still propagates its original failure; null propagation is not exception suppression. Ordinary source dot access expresses this behavior without C# conditional-access punctuation.

CS-ACCESS-002. A value-type property/field reached through a potentially missing read receiver has the corresponding real CLR nullable result, preserving absence instead of substituting zero, false or another default. A compatible non-null fallback under ?? produces the ordinary nonnullable value type, with contextual conversion of the fallback. Reference results retain actual null from a reached getter or external API; String's effective declaration default does not normalize that value. These read rules do not introduce an Optional runtime wrapper or change stored operands.

CS-ACCESS-003. Ordinary instance action/method calls and writes require a present receiver. A missing receiver or intermediate receiver follows normal .NET NullReferenceException behavior, rather than skipping execution; ordinary indexing retains CS-INDEX-001's required receiver/location contract. Do not swallow or relabel failures thrown by an executed getter, setter, method or imported implementation. A definitely missing receiver may be diagnosed separately; no mandatory proof of non-null for every call, custom missing-segment exception object/message or inspection of opaque method internals is required. This instance-access rule does not replace explicitly selected library extension/null-source contracts such as CS-SEQUENCE-011, or ordinary static API argument behavior. Required use and conditional use_if retain their separately specified scope behavior.


Specification contents · Previous: 5. Declarations and scope · Next: 7. Statements and control flow