Osy#betaRun it on Osyrin: osyrin.com

Reference / Types

Constant expressions

[Label("Awaiting " + "review")] // folded at compile time

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.

stable5 examples compiled by CItypesauthoring

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#

[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#

Where a constant is 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 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#

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.

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);
}
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:

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#

$"…" 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#

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

enum ReviewState {
  [Label("Awaiting " + "review")] Pending,
  [Label("Signed " + "off " + "by legal")] Approved,
}

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

entity Doc {
  [Pattern("^DOC-" + "[0-9]+$", "use " + "DOC-1234")] string Code;
  ReviewState State;
}

Neither of these is constant, and both are refused:

[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#

Related

Optional and required members

A member is required or optional by how you spell its type. A bare value type with a natural zero reads that zero; a…

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…

enum

A fixed set of named values, used as a member type. Stored as a number by default, or as the member's own name with…