# Constant expressions

> Some places take a value that must be known at compile time — an attribute argument, a config setting, a workflow message. Adjacent string literals joined with `+` are folded before that check, so you can split a long sentence across lines. Anything that is not constant is still refused.

<!-- id: types-constant-expressions · area: types · stability: stable · html: https://osysharp.com/reference/types/constant-expressions/ -->

## Summary        {#summary}

A few places in Osy# take a value that has to be known while the app is being compiled, not while it is running — an
attribute argument, a setting in a config block, an enum member's display label, a workflow message. Those places
require a **constant**.

A string literal is the obvious constant. So is a chain of string literals joined with `+`: the compiler folds
`"Awaiting " + "review"` into `"Awaiting review"` before it checks that the value is constant, exactly as C# does.
That means a long sentence can be split across lines for readability without the compiler objecting.

What is *not* constant is still refused, and for the real reason. A value that depends on a member, a parameter or a
runtime call is not known at compile time, so it cannot go where a constant is required — no matter how it is spelled.

## Signature      {#signature}

```osy syntax
[Label("Awaiting " + "review")]        // folded → "Awaiting review"
[Label("Signed " + "off " + "by legal")]   // any length of chain folds

[Pattern("^DOC-" + Code)]                // REFUSED — `Code` is not a constant
[Pattern($"^DOC-[0-9]+$")]               // REFUSED — an interpolated string is not a literal
```

## Description    {#description}

### Where a constant is required   {#where-required}

These are the places that read a value at compile time rather than evaluating it at run time:

- **Attribute arguments** — `[Label]`, `[Pattern]`, `[ExternalName]`, a constraint's optional message, and a numeric
  bound such as `[Max(int.MaxValue)]` or `[Range(0, long.MaxValue)]`.
- **Enum member labels** — `[Label]` and `[Icon]` on a member.
- **Config settings** — the values inside a config block, and the entries of a string list.
- **Workflow messages** — a terminal's or a requirement's `Message`.
- **Agent and tool descriptions** — a `Description` on a tool or agent, which the model reads.

In each case the value is baked into the application's metadata when it is compiled. There is no later moment at which
a non-constant could be evaluated, which is why the requirement exists.

### Folding   {#folding}

Folding is *left-nested*, mirroring how `+` associates: `"a" + "b" + "c"` folds only because each step folds. A chain
with one non-constant operand is not a constant at all, and returns nothing rather than a partial result — so
`"Hello " + name` is refused rather than quietly becoming `"Hello "`.

Folding happens during the compile, not in the parsed source. Tooling that reads your code — hover, go-to-definition,
selection — still sees the expression you wrote, with the `+` intact.

### Constant members of the built-in types   {#constant-members}

The numeric and text types carry the constant members C# gives them, written without parentheses:
`int.MinValue`/`int.MaxValue`, `long.MinValue`/`long.MaxValue`, `decimal.Zero`/`One`/`MinusOne`/`MinValue`/`MaxValue`,
`double.MinValue`/`double.MaxValue` and `string.Empty`. The .NET type names work too (`Int32.MaxValue`,
`String.Empty`). Each is folded to the value it names, so it can go anywhere a value of its type can — in a function,
in a query filter, in a component — and it can also be a member's default. `Guid.Empty`,
`DateTime.MinValue`/`MaxValue`, `DateOnly.MinValue`/`MaxValue`, `TimeOnly.MinValue`/`MaxValue` and
`TimeSpan.Zero`/`MinValue`/`MaxValue` are values too, and work in the same places.

The largest of the time values are the largest a stored value can HOLD, because a stored date or time keeps
microseconds and .NET's own maximum carries a finer tail: `DateTime.MaxValue` is `9999-12-31 23:59:59.999999`,
`TimeOnly.MaxValue` is `23:59:59.999999`, and `TimeSpan.MaxValue` / `MinValue` are `±10675199.02:48:05.4775800`. Each
has that same value in a function, in a query filter, as a default and after a round trip through the database.

```osy title="a constant member in an expression" test app=types-constant-expressions
int Lowest(int[] values) {
  var lowest = int.MaxValue;
  foreach (var v in values) {
    if (v < lowest) { lowest = v; }
  }
  return lowest;
}

string Initials(string name) {
  return name.Length == 0 ? string.Empty : name.Substring(0, 1);
}
```

```osy title="the date, time and duration constants" test app=types-constant-expressions
bool IsOpenEnded(DateOnly until) {
  return until == DateOnly.MaxValue;
}

TimeSpan NoLimit() {
  return TimeSpan.MaxValue;
}
```

A numeric constant member is a constant ATTRIBUTE argument too, so a bound can say what it means rather than spell
out its digits:

```osy title="a numeric constant as an attribute argument" test app=types-constant-expressions
entity Quota {
  [Range(0, long.MaxValue)] long Bytes;
  [Max(int.MaxValue)] int Files;
}
```

A mistyped one is refused with its real name offered — `int.Max` suggests `MaxValue`.

### Interpolated strings are deliberately not constants   {#interpolation}

`$"…"` is refused where a constant is required, even when it happens to contain no holes. The `$` prefix declares an
intent to interpolate, and a place that needs a compile-time value should say so plainly rather than accept a form
whose purpose is to be computed. Write a plain literal, or a `+` chain of them.

## Examples       {#examples}

A long label split across two lines, and a three-part chain:

```osy title="splitting a long label" test app=types-constant-expressions
enum ReviewState {
  [Label("Awaiting " + "review")] Pending,
  [Label("Signed " + "off " + "by legal")] Approved,
}
```

An attribute whose pattern *and* whose failure message are both split:

```osy title="a constraint written across lines" test app=types-constant-expressions
entity Doc {
  [Pattern("^DOC-" + "[0-9]+$", "use " + "DOC-1234")] string Code;
  ReviewState State;
}
```

Neither of these is constant, and both are refused:

```osy title="✗ a member reference, and an interpolation" syntax
[Pattern("^DOC-" + Code)]     // depends on a member — not known at compile time
[Pattern($"^DOC-[0-9]+$")]    // an interpolated string is not a literal
```

## See also       {#see-also}
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — how a member's type spelling decides whether a value must be supplied
- [constraints](https://osysharp.com/reference/entity/constraints/) — the attributes that take a pattern and a message
- [enum](https://osysharp.com/reference/enum/declaration/) — where member labels are declared
