Types, defaults and actions
DirectSharp uses CLR types, generic interfaces, delegates and normal .NET overload binding. The source syntax presents those contracts in DirectSharp terms.
Declarations and generic types
let count = 0
let title as String = "Invoice"
let readonly customer = LoadCustomer(id)
let rows = new Global.System.Collections.Generic.List <of String>()
The application fragment assumes LoadCustomer and id. Every local needs an initializer. A readonly binding cannot be rebound or passed as a writable reference; its referenced object's members can still change. Local/parameter shadowing is prohibited. Ordinary loop bindings are readonly.
Generic arguments and parameters use <of ...>:
Dictionary <of String, List <of Customer>>
This is a type fragment with the named collections and Customer in scope. The space before < is formatter style. Generic method inference follows normal applicable argument/type relationships. A type parameter occurring only in the return type does not gain special inference from the assignment target.
The generic body can require capabilities with where T inherits BaseType, where T implements InterfaceType and where T has constructor. These are real CLR constraints, not checks for similarly named members. The constrained import-draft example shows complete source, C# declarations and the constructor/inference boundaries.
Absence and defaults
nothing is actual absence. default is the effective default of a known target type. They are not synonyms.
| Target | Ordinary effective default |
|---|---|
| Numeric / Boolean / Char | Zero / false / UTF-16 zero |
| String | Empty string |
| Built-in collections | Empty collection |
| Ordinary class | nothing, unless customized |
| Interface / delegate / nullable value / Object | nothing |
Targetless default is invalid; let value = nothing can infer Object while preserving absence. Custom defaults can be intrinsic or supplied by one visible external definition. Conflicting visible external definitions are errors; imports do not decide the winner. Locked defaults prevent replacement. Default/preset cycles must be rejected.
let caption as String = default
let customer as Customer = nothing
Reference C# for these known targets:
string caption = "";
Customer? customer = null;
A universal C# default(T) lowering would lose the String default. For supported authored nullable arithmetic, default substitution is local to that operation and leaves stored absence intact. Imported operators retain their CLR behavior.
value ?? fallback evaluates the fallback only when the value is absent. ?? throw is an immediate presence check. require/ensure failure customization instead uses otherwise; the constructs serve different purposes.
classValue ?? default uses normal effective-default resolution. If the class has no visible custom default, its effective default is nothing; the expression is legal and can remain absent. It does not synthesize an instance or invoke a parameterless constructor. A valid custom default is evaluated only when the source is missing, under the usual visibility/uniqueness rules.
An initializer using ?? throw does not make the new local permanently required. Later assignments follow normal type and readonly rules; they receive no automatic guard from the earlier check. Non-nullable value targets still cannot receive nothing, while a mutable reference local can retain actual absence.
Declare a nullable value deliberately
A nonnullable CLR value cannot contain nothing. Nullable <of T> is actual System.Nullable<T>, usable in parameters, results, members and locals. This declaration infers nullable Integer once, at declaration:
let amount as Integer = nothing
amount = 125
amount = nothing
let creditLimit as Global.System.Nullable <of Decimal> = nothing
A local initialized with let count as Integer = 0 keeps nonnullable Integer; later assigning nothing is a type error. Runtime values do not change the local's type. Use explicit Nullable <of Decimal> for an action contract that may return absent credit, and preserve real nullable signatures of imported APIs. String/class references can already contain nothing; Nullable <of String> violates the CLR constraint.
C# nullable-reference annotations alone do not make passing a missing reference a DirectSharp compilation error or default warning. The native method may still reject it at runtime. String's effective empty default does not normalize an actual null. See the exact nullable rules and generic selection account.
Read an absent receiver
Ordinary property/field reads propagate nothing through a missing receiver. Given a Customer with Address.City and Decimal CreditLimit, these action fragments use normal dot syntax:
let city = customer.Address.City
let credit = customer.CreditLimit
let available = customer.CreditLimit ?? 0
If customer or Address is absent, city is nothing. A potentially missing receiver gives credit a real nullable Decimal representation; available is nonnullable Decimal because its fallback supplies zero. A present zero or empty String remains present. Reached getters run once and their exceptions propagate; the compiler does not silently turn every failure into absence.
Instance calls, writes and indexing remain required operations. An absent customer does not make customer.Activate(), an assignment to customer.Name or an index request silently disappear. Use an explicit condition or use_if when an operation should be skipped. Selected library helpers keep their own null-source contracts, including the strict/Try sequence distinction. C# conditional-access punctuation is not needed for ordinary DirectSharp read chains. See the exact read/required-access rules and reference C# cases.
Action declarations and results
public action CalculateFee(amount as Decimal)
returns Decimal
require amount >= 0
return amount * 0.02
end action
returns declares the result type; return exits the action. An inline run body is a separate callable boundary: its return completes only that branch. A synchronous call used as a statement may ignore a result; an awaitable call still requires explicit await/task_of/run intent. That does not make a result-producing action resultless or change its ABI.
Reference C#:
public decimal CalculateFee(decimal amount)
{
if (amount < 0)
throw new global::System.ArgumentException("Amount must not be negative: amount >= 0", nameof(amount));
return amount * 0.02m;
}
Actions can have positional and named arguments. Named arguments use name: expression; ordinary argument compatibility and evaluation rules still apply. Substantial callbacks can be named or local actions rather than growing anonymous syntax.
Callable values
Use action(...) as a callable type. action(Customer) is resultless; action(Customer) returns Boolean has a Boolean result.
action IsPositive(value as Integer)
returns Boolean
return value > 0
end action
let check as action(value as Integer) returns Boolean = IsPositive
let accepted = check(value: 42)
A typical CLR representation is a delegate:
bool IsPositive(int value) => value > 0;
global::System.Func<int, bool> check = IsPositive;
bool accepted = check(42);
Parameter names in a callable type supply its named-invocation surface, but are not part of callable type identity. Assignment of another compatible action does not change those names. An unnamed callable signature permits positional invocation only. Callable type parameters do not carry optional/default arguments.
Compatibility considers parameter count, type, order, applicable ref-kind and return contract. Common signatures can use Action/Func; unusual CLR contracts can use appropriate custom delegate types without introducing a second callable model.
Async actions and iterators
| Source declaration | Normal CLR contract |
|---|---|
action ... returns T |
T |
async action ... returns T |
Task-equivalent of T |
action ... yields T |
IEnumerable of T |
async action ... yields T |
IAsyncEnumerable of T |
Ordinary actions, including public actions, may infer async implementation from required suspension. Their emitted CLR return type can consequently change when the body changes. Optional explicit async fixes the asynchronous contract independently of the body. Interface/overridable contracts and overrides preserve their declared signatures. Inference does not automatically add the explicit async cancellation contract.
An iterator's contract is explicit through yields; an awaitable call alone does not turn a synchronous iterator into an async iterator. Ordinary for_each recognizes an async enumerable and uses asynchronous enumeration. See async and cancellation.
Characters and Unicode scalars
A CLR Char is one UTF-16 code unit. Fixed single-unit Chars values such as Tab have Char type; NewLine and CRLF have String type. Named Unicode values are String text.
| Helper | Contract |
|---|---|
Chars.FromCodeUnit(value) |
Long input; 0–65535 becomes Char; other values throw ArgumentOutOfRangeException; surrogate units are allowed |
Chars.CodeUnit(value) |
Obtain the Integer code-unit value of a Char |
Unicode.FromCodePoint(value) |
Long input; String for a valid scalar; invalid values throw ArgumentOutOfRangeException |
Unicode.CodePoint(text) |
Integer value of exactly one valid scalar in the entire String; empty/multiple/malformed throws ArgumentException |
Unicode.CodePoints(text) |
Strict scalar traversal; isolated surrogates throw ArgumentException |
FromCodeUnit and FromCodePoint accept Long; an Integer argument widens normally. Validate the full Long before narrowing so a large invalid input cannot wrap into a valid code unit/scalar. CodeUnit and CodePoint return Integer; every valid result fits, and callers can widen normally to Long.
Valid scalars are 0–0x10FFFF except 0xD800–0xDFFF. A valid surrogate pair represents one supplementary scalar. Both text-decoding helpers reject nothing with ArgumentNullException; CodePoints performs that check at the call and returns a deferred streaming IEnumerable <of Integer>. Each enumeration owns its traversal position; malformed input fails when reached, possibly after a valid prefix. Early exit leaves the unvisited suffix undecoded. Empty CodePoints has no values. Neither replaces malformed text, normalizes it or treats a grapheme as necessarily one scalar. Ordinary String iteration still yields Char units. See the specification and reference decoding.
Types, nothing and default
List <of String> illustrates generic notation. nothing means actual absence; default means the effective default of a known target type.
| Situation | Result/requirement |
|---|---|
| String default | Empty string |
| Numeric / Boolean default | Zero / false |
| Built-in collection default | Empty collection |
| Ordinary class without custom default | nothing |
| Custom default | Intrinsic definition, or a single visible external definition |
lock default |
Preserve absent fallback; prevent external replacement |
| Multiple visible external defaults | Compile-time ambiguity |
let value = default with no target |
Compile-time error |
CS-NULL-001–CS-NULL-005 and CS-DEFAULT-001–CS-DEFAULT-005 define the boundary in chapter 4. Defaults cannot universally lower to C# default(T). Supported authored nullable arithmetic substitutes defaults locally to an operation; stored absence and imported CLR operator behavior are preserved. No persistent arithmetic propagation policy is attached to an assigned value.
Class coalescing with ?? default uses that same effective default. Without a visible custom default, an absent class reference remains nothing; no custom-default declaration or automatic construction is required. Default ambiguity and compatibility rules still apply, and a targetless bare default is invalid.
Local declarations
let count = 0
let readonly customer = LoadCustomer(id)
let from customer
name = .Name
end let
Every local requires an initializer. Readonly prevents rebinding and writable by-reference use, not mutation of the referenced object. Loop bindings are readonly. Nested local/parameter shadowing is prohibited. Extraction requires explicit local names and source-relative dots.
The extraction header may be a source expression such as LoadCustomer(id). Evaluate it once before extraction; leading-dot accesses use that captured object and declarations evaluate in written order. Ordinary typing and async-context rules apply; no copy or disposal is implied.
Let from requires the captured source to be present and fails before extracting if it is nothing. Let_if from/end let_if instead assigns nothing to every declared local on a missing source without evaluating any extraction expression. A present source extracts normally; evaluated exceptions propagate in both forms. Conditional reference/String locals preserve absence; non-nullable value types use native CLR Nullable<T>, including explicit as T annotations. Already-nullable types are not nested, and the type is fixed at declaration. Unsupported CLR absence representations are errors. See CS-DECL-009 in chapter 5.
Rules CS-DECL-001–CS-DECL-006, diagnostics and readonly lowering: chapter 5.