On this page

16. Predicates and pattern matching

Predicates classify, test or match a value. They produce Boolean results and support ordinary negation. Successful type/pattern tests can establish narrower types or scoped captures.

16.1 Absence, emptiness, whitespace and type

CS-PREDICATE-001. is nothing tests actual absence only; is not nothing negates it. is empty includes absence, a meaningful effective type default under applicable equality, or a collection/sequence with no elements. Zero, false, Guid.Empty, DateTime.MinValue and an empty String are empty. A whitespace-only nonempty String is not empty; whitespace is a separate String predicate.

For a sequence, emptiness is an Any-style probe, with an equivalent count/shape fast path permitted when it preserves observable behavior. For an ordinary reference type without meaningful default/value equality, only nothing is empty. No structural equality is invented merely to implement this test. Is default is not a predicate; use ordinary comparison with contextual default when that specific comparison is wanted.

CS-PREDICATE-002. String whitespace follows IsNullOrWhiteSpace-style behavior. is in tests membership in a compatible collection/range or String containment where applicable. String containment is case-insensitive by default; in_case_sensitive selects its String-only case-sensitive form. Collection membership retains its selected comparer contract rather than acquiring a String-only interpretation.

CS-PREDICATE-003. is a T tests runtime assignability and flow-narrows the proven subject on its successful path, including contextual continuation when identity is preserved safely. It does not invoke value conversion. is exactly T tests exact runtime type, rather than assignability. is same_as tests reference identity, not value equality. Corresponding is not forms negate the predicate.

if customer is a PreferredCustomer and .IsActive then
    customer.ApplyBenefits()
end if

PreferredCustomer and ApplyBenefits are ordinary application symbols. The type proof permits their members in the true branch. A getter returning a different object on each read is not an excuse to treat distinct receiver evaluations as one stable narrowed value.

16.2 String predicates and ordinary Boolean members

CS-PREDICATE-004. String relationship predicates are containing, starting_with and ending_with, with their corresponding _case_sensitive variants. Default comparisons are deterministic and culture-stable. Classification forms include ascii, numeric, integer, decimal, iso_date and parsable_as Type; they do not return parsed values. Iso_date is the narrow yyyy-MM-dd calendar-date form. Parsable_as requires a recognized parsing contract; an unsupported target is a compilation error, not arbitrary conversion discovery.

CS-PREDICATE-007. Numeric String classifiers use invariant ASCII forms after removing surrounding whitespace according to ordinary CLR String.Trim semantics. Absence, empty text and whitespace-only text classify false. A leading ASCII plus/minus sign is permitted. Interior whitespace, grouping separators, currency signs, base prefixes, underscores, Unicode digits and NaN/infinity names are not numeric classifier forms.

The accepted shapes are:

Digits      := [0-9]+
Sign        := "+" | "-"
IntegerText := [Sign] Digits
DecimalText := [Sign] Digits ["." Digits]
NumericText := DecimalText [("e" | "E") [Sign] Digits]

Integer-shaped text is also a decimal/numeric shape. Decimal excludes scientific exponents; numeric permits them. A present decimal point requires digits on both sides. These rules constrain classifier inputs; they do not change source literal syntax, imported Parse APIs or target-specific parsable_as contracts.

CS-PREDICATE-008. Accepted lexical shape is necessary but not sufficient for numeric String classification. is integer additionally requires representability as Long; is decimal requires representability as Decimal; is numeric requires a finite Double result. Apply these checks using the invariant input grammar above, not an arbitrary imported parser's default styles. Out-of-range/unrepresentable values classify false rather than throwing. A Double parse yielding infinity is false. Negated predicates negate that Boolean result.

let ordinary = "42" is integer
let tooLarge = "999999999999999999999999999999" is integer
let infinite = "1e1000" is numeric
// ordinary is true; tooLarge and infinite are false

These fixed classifier ranges do not choose a destination for conversion. Range gating uses normal native numeric parsing precision/rounding; it does not require an exact text-to-value round trip. A Double underflow that parses to finite zero passes the numeric range gate. Use parsable_as with a specific target when that target's recognized parsing contract is the question; imported Parse/TryParse calls retain their own styles, culture and failure behavior. A text may satisfy one numeric classifier while failing another classifier's range or grammar.

Text Integer shape Decimal shape Numeric shape
" +12 " Yes Yes Yes
"-12.50" No Yes Yes
"1e3" No No Yes
".5", "5." No No No
"1,000", "1_000", "NaN" No No No
nothing, "", " " No No No
if fileName is ending_with ".csv" and
    fileName is starting_with "report-" then
    ImportReport(fileName)
end if

CS-PREDICATE-005. Boolean Is*/Has* properties and actions can expose conversational predicates using a real word boundary and the original declared spelling. IsValidEmail becomes is valid_email; HasDebt becomes has debt. Only Boolean members qualify. PascalCase, camelCase and snake_case prefixes are recognized; unrelated names such as Island or Hash do not qualify.

CS-PREDICATE-006. IsNot*/HasNot* members expose their own negative results directly. IsNotClosed maps to is not closed, not a generated call to not IsClosed. Their logical consistency is the type author's responsibility. Built-in predicate meanings take precedence over derived aliases; a colliding ordinary member remains directly callable. No can prefix, generic predicate declaration or built-in domain validator is implied.

16.3 Wildcard and sequence patterns

CS-PATTERN-001. Is like explicitly performs wildcard matching; equality does not interpret wildcard syntax. Star matches zero or more items/characters; question mark matches exactly one. String matching is case-insensitive unless like_case_sensitive is selected. Regex is a separate facility.

CS-PATTERN-002. Sequence patterns require suitable ordered/indexable/countable targets. Arbitrary one-shot IEnumerable sources must not be enumerated invisibly to manufacture indexed pattern access.

CS-PATTERN-003. A sequence level permits at most one variable-length star section, captured or ignored. Nested levels apply the limit independently. Fixed-position relative comparisons and supported nested sequence patterns are permitted.

CS-PATTERN-004. let name = pattern introduces a capture only in the successful matching scope. A question-mark capture selects one element; a star capture selects its matched sequence with the natural representable target element/sequence type. Captures cannot leak into paths where matching failed.

if amounts is like [> 0, let middle = *, < 1000] then
    ProcessAmounts(middle)
end if

For an array target, the corresponding C# list-pattern shape is:

if (amounts is [> 0, .. var middle, < 1000])
    ProcessAmounts(middle);

The capture is not created on failure. A different supported sequence type must preserve its applicable indexing/capture contract; the illustration does not make every enumerable an array.

String captures use double-curly let embedding:

if fileName is like "report-{{let name = *}}.csv" then
    Print(name)
end if

The name local has String type. In this context the let embedding is a capture, not ordinary interpolation.

CS-PATTERN-005. Wildcard escapes backslash-star, backslash-question and doubled backslash select literal star, question mark and backslash. These are matcher escapes; backslash is not a general String lexer escape. SQL percent/underscore wildcard meanings and regex character classes are not imported into like.

16.4 Regex captures

CS-REGEX-001. Is match uses .NET regex syntax. Ordinary matching is case-insensitive; match_case_sensitive selects case-sensitive matching. Wildcard capture embedding is not regex syntax.

CS-REGEX-002. Automatic named locals require a compile-time-known pattern. Their scope is the successful branch. A dynamic pattern introduces no compiler-invented names; ordinary .NET Match/Group APIs remain available.

CS-REGEX-003. A non-repeat-capable named capture has String type. A nonparticipating group supplies nothing; a participating empty group supplies an empty String.

CS-REGEX-004. A repeat-capable named capture has List <of String> type, containing every .NET capture in order. Zero captures after a successful match give an empty list, not nothing. Repeatability is structural and established at compile time; duplicate names in mutually exclusive branches alone do not make a group repeat-capable.

if text is match "^(?<id>[0-9]+)(,(?<id>[0-9]+))*$" then
    for_each value in id
        ProcessIdentifier(value)
    end for_each
end if

For 10,20,30, id contains the three strings in order. The local is a list throughout this successful branch; it does not change its static type according to a particular match's capture count.

CS-REGEX-005. Only names satisfying the ordinary ASCII identifier grammar are promoted. A regex-engine-valid name outside that grammar does not invalidate the regex; it introduces no automatic local.

CS-REGEX-006. A promoted keyword name uses ordinary @ escaping. Promotable names differing only by case are a compile-time ambiguity; the compiler must not silently select or rename one. Unnamed groups do not introduce locals.

if text is match "^(?<class>[A-Z]+)$" then
    Print(@class)
end if

16.5 Reference lowering and diagnostics

After a successful .NET Match, capture projections have different absence contracts:

string? optional = match.Groups["optional"].Success
    ? match.Groups["optional"].Value
    : null;
List<string> repeated = match.Groups["id"].Captures
    .Cast<Capture>()
    .Select(capture => capture.Value)
    .ToList();

The code assumes ordinary collections/LINQ/regex imports. Regex option, timeout and caching policies remain library concerns; this projection does not choose them. A convention predicate lowers to its resolved ordinary Boolean member, and reference identity to the corresponding CLR identity test.

Reject a predicate inappropriate for its receiver, a parsing target without a recognized contract, more than one variable-length sequence wildcard at one level, an unsupported sequence target, capture use outside successful scope and case-insensitive promoted-name collisions. Keyword captures require identifier escaping. Matcher-specific backslash handling must not change how the source lexer handles unrelated Strings.


Specification contents · Previous: 15. Collections, enumeration and consumption · Next: 17. Async, iteration and concurrency