# The standard library — and where each call runs

> The pure standard library, and the one fact about it that changes how your app feels: 126 of its 155 methods run in the BROWSER, with no round trip. The rest that do not are held there by ONE thing — they need a secret the browser must never hold (a signing key, a KMS key, a password hash). Nothing here needs a capability.

<!-- id: stdlib-index · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/index/ -->

## Summary        {#summary}
The standard library is the set of **pure** calls available in any function body — text, numbers, dates, encoding,
hashing. No `use`, no capability, no ceremony.

The thing worth knowing about it is **where each call runs**. Osy# decides that for you: a function whose body the
browser can execute runs *in the browser*, and never makes a network call. **126 of the 155 stdlib methods can.** So

```osy syntax
var label = Text.Upper(name);              // runs in the browser
var price = Convert.ToString(total, "F2"); // runs in the browser
var due   = Date.AddDays(start, 30);       // runs in the browser
```

…are all free. You do not opt in, and there is no annotation. Write the code; the compiler works out where it can
run.

## Where each call runs        {#description}
**The default is the client, and the goal is that it always is.** A method is kept on the server only when running it in
the browser would give a *different or unsafe answer* — never because it was hard to port. There is one reason that is a
true floor, and a shrinking frontier of methods on their way to the client.

### 1 — It needs a secret the browser must never hold        {#host-authority}
This is the floor — a handful of `Security.*` and `Crypto.*` operations, and the only ones that can *never* move. The
client is the **user's own machine**, so the JWT signing key, the KMS encryption key, an HMAC key and a password
hash's cost/salt are precisely the things it must not see. Running these in the browser would not be slow — it would
either **leak the secret** or let the client **forge authority** (a client-minted token is self-issued). The round
trip is the security boundary, not a performance cost.

```osy title="the handful that can never leave the server" syntax
Security.HashPassword(pw)     // server — the cost factor and the salt RNG are the host's
Security.IssueJwt(claims)     // server — signed with the host's key; a client could forge one
Crypto.Encrypt(secret)        // server — the KMS key never reaches the browser
```

Host-authoritative randomness sits here too: `Security.RandomId` / `RandomHex` must come from the host's RNG, or an
"unguessable" id is guessable.

The clock is **not** one of these, and it is worth saying so directly because it looks like it should be. `DurableClock.Now`,
`DurableClock.UtcNow` and `DateTime.UtcNow` **run on the client.** A `DateTime` here is a UTC *instant*, not a wall-clock
reading, so a browser and the server name the same value — 9am in Frankfurt *is* 5pm in Tokyo, the same instant — and
reading it in the browser costs no round trip. (Test pinning still works, and a resumed durable function still sees a
consistent instant, because this engine resumes from a saved point rather than re-running your function from the top.)

```osy title="the clock is NOT one of them — it reads in the browser" syntax
var now  = DurableClock.Now;                        // client — a UTC instant, read in the browser
var due  = Date.AddDays(order.Placed, 30);   // client — pure arithmetic on a value in hand
var year = Date.Year(order.Placed);          // client
```

### 2 — The browser cannot yet reproduce it faithfully        {#fidelity}
This has shrunk to essentially one case, and it is a specific fidelity gap, not a law of nature: a `Double` handed to
`Convert.ToString(value, format)` still renders on the server, because .NET produces its digits from the value's exact
*binary* expansion. (`Decimal` — this platform's money type, and what a format specifier is almost always for — already
formats in the browser.)

Everything that used to sit here has moved to the client: `WebUtility.HtmlDecode` now ports .NET's *own* algorithm and
entity table (not the browser's, which decodes a different set — it accepts `&copy` *without* the semicolon where .NET
does not); `WebUtility.HtmlEncode` ships its five-character escape set; and the unkeyed hashes `Crypto.Sha256Hex` /
`Crypto.Md5Hex` run in the browser too, since neither holds a secret.

## Which two calls decide per call site?        {#conditional}
Two methods are not client-or-server by *name* — the **arguments** decide, so the same method may run in the browser at
one call site and on the server at the next.

**`Convert.ToString(value, format)`** — and the `$"{total:F2}"` that lowers onto it. It runs in the browser when the
format is a *literal* — either a *standard* specifier (`F2`, `N0`, `C`, `P1`) or a *custom* digit pattern
(`"#,##0.00"`, `"0.00"`, `"00000"`) — over a `Decimal` or `Int32`. A format built at *runtime*, or a `Double`, still
goes to the server.

```osy syntax
var a = Convert.ToString(total, "F2");        // client — literal, standard, decimal
var b = Convert.ToString(total, "#,##0.00");  // client — the compiler parses the pattern into a plan and ships it
var c = Convert.ToString(total, fmt);         // server — a runtime format string the compiler cannot see
```

A *literal* custom pattern (`b`) is a compile-time constant, so the compiler parses it **once** into a plan — how many
digits, whether to group — and ships the plan; the browser executes arithmetic, never a parser (the same trick regular
expressions already use). A pattern that reaches past the digit-placeholder core — section separators, scaling,
percent, scientific, embedded literals — stays on the server, where .NET renders it correctly.

**A v1 boundary worth stating plainly:** client-side custom-pattern formatting works only for a **compile-time literal**
pattern, because the mechanism is the *compiler* parsing it into a plan. A **runtime-built** format string (`c` — where
the format is a variable) the compiler cannot see, so it stays on the server; today translating it would mean shipping
a second .NET-format parser to every browser. That is a v1 limitation, not a permanent one: a future version could ship
a small client-side format *interpreter* so a runtime-built pattern also runs in the browser. Until then, `c` is a
round trip and returns the right string.

**`JsonSerializer.Serialize(value)`** runs in the browser for a scalar or a list of scalars. Hand it a `class` or an
entity and it goes to the server: naming and ordering the members requires the model, which the browser does not hold.
Guessing at a member order would produce JSON that parses and is wrong.

## Does this change my code?        {#does-it-change-my-code}
**No.** Where a call runs never changes its *answer* — the client arms are pinned against the real server by an oracle
suite, down to the details C# gets opinionated about (`Math.Round(2.5)` is `3`, away from zero, on both sides;
`Uri.EscapeDataString` escapes `!'()*`, which `encodeURIComponent` does not).

It changes only how the app *feels*. A form that formats a price, validates a pattern and computes a due date now does
all of it without touching the network. And if you write a function that mixes a client-runnable call with a
server-only one, the function simply runs on the server — the answer is still right, it just costs a round trip.

Here is the surface actually running — and the two answers C# has an opinion about, pinned. The client arms are held
to exactly these, so the assertions below are true **wherever the function ends up running**:

```osy title="a function that never touches the network" test app=stdlib-index
// Every call in this body is client-runnable, so the whole function runs in the browser. No annotation, no opt-in.
string PriceLabel(decimal total) {
  return Text.Upper("total") + ": " + Convert.ToString(total, "F2");
}
```

```osy title="the same answer on either side" run app=stdlib-index
[Test]
void The_stdlib_gives_the_same_answer_wherever_it_runs() {
  Assert.Equal("TOTAL: 12.50", PriceLabel(12.5m));           // two decimals, not "12.5"
  Assert.Equal("ACME", Text.Upper("acme"));

  // AWAY FROM ZERO, not banker's rounding — `Math.Round(2.5)` is 3. A client that used JS's Math.round
  // would agree here and disagree on 3.5, which is how a half-cent walks into an invoice.
  Assert.Equal(3m, Math.Round(2.5m));
  Assert.Equal(4m, Math.Round(3.5m));

  // A literal custom pattern — the money-grid case — runs in the browser too: the compiler parses "#,##0.00"
  // into a plan and ships it. Grouped, two decimals, rounded away from zero on the exact digits.
  Assert.Equal("1,234.57", Convert.ToString(1234.5678m, "#,##0.00"));

  // The deterministic date surface: pure arithmetic on a value already in hand. Runs in the browser.
  var due = Date.AddDays(DateTime.Parse("2026-01-01"), 30);
  Assert.Equal(31, Date.Day(due));
  Assert.Equal(2026, Date.Year(due));
}
```

## Every method, and where it runs        {#the-table}
Generated from the compiler's own allowlist and **checked on every build** — if a method moves, is added, or is
removed, this table fails the build until it is right. It cannot go stale.

**103 run on the client · 9 are server-only · 2 decide per call site.**

#### `Clock`

| Method | Runs on | Why not the client |
|---|---|---|
| `Backoff.Cap` | server | bounds a retry policy, which only the durable engine reads |
| `Backoff.Exponential` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.Fixed` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.Jitter` | server | bounds a retry policy, which only the durable engine reads |
| `Backoff.Linear` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.MaxAttempts` | server | bounds a retry policy, which only the durable engine reads |
| `Convert.AmbientAppField` | client | ships as an appField node — the client-safe app identity (Slug / Domain / BaseUrl) from the boot app ambient (the internal carrier of `App.Slug`/`Domain`/`BaseUrl`) |
| `Convert.AmbientCulture` | client | ships as an ambientCulture / cultureFormatAmbient node — the viewer's culture token from the metadata payload (the internal carrier of Session.CurrentCulture) |
| `Convert.AmbientUserField` | client | ships as a currentUserField node — a scalar of the viewer's principal from the boot bag (the internal carrier of `Session.CurrentUser` fields) |
| `Convert.FromBase64String` | client | — |
| `Convert.ToBase64String` | client | — |
| `Convert.ToBool` | client | — |
| `Convert.ToDecimal/1` | client | — |
| `Convert.ToDecimal/2` | conditional | gated per call site — a literal declared culture ships as a cultureParse node; a dynamic culture stays server |
| `Convert.ToDouble` | client | — |
| `Convert.ToInt/1` | client | — |
| `Convert.ToInt/2` | conditional | gated per call site — a literal declared culture ships as a cultureParse (int profile) node; a dynamic culture stays server |
| `Convert.ToInt64` | client | — |
| `Convert.ToString/1` | client | — |
| `Convert.ToString/2` | conditional | gated per call site — a literal standard specifier or a plannable custom pattern over a Decimal/Int32 ships; a runtime format, a Double, or an unplannable pattern stays server |
| `Convert.ToString/3` | conditional | gated per call site — a literal supported format + a literal (declared) culture over a Decimal/Int32 ships as a cultureFormat node keyed by token; anything else stays server |
| `Crypto.Decrypt` | server | decryption uses the app's platform-managed KMS key, which the client never holds |
| `Crypto.Encrypt` | server | encryption uses the app's platform-managed KMS key, which the client never holds |
| `Crypto.FixedTimeEquals` | server | a constant-time compare is only meaningful next to the secret it guards |
| `Crypto.HmacSha256Hex` | server | an HMAC is keyed; the key is the host's and must not reach the browser |
| `Crypto.Md5Hex` | client | — |
| `Crypto.Sha256Hex` | client | — |
| `Date.AddDays` | client | — |
| `Date.AddHours` | client | — |
| `Date.AddMinutes` | client | — |
| `Date.AddMonths` | client | — |
| `Date.AddYears` | client | — |
| `Date.Date` | client | — |
| `Date.Day` | client | — |
| `Date.DayOfWeek` | client | — |
| `Date.Hour` | client | — |
| `Date.Minute` | client | — |
| `Date.Month` | client | — |
| `Date.Second` | client | — |
| `Date.Year` | client | — |
| `DateOnly.FromDateTime` | client | — |
| `DateOnly.New` | client | — |
| `DateOnly.Parse` | client | — |
| `DateTime.MaxValue` | client | — |
| `DateTime.MinValue` | client | — |
| `DateTime.New` | client | — |
| `DateTime.Parse` | client | — |
| `DateTime.ParseExact` | conditional | gated per call site — a literal custom yyyy-pattern + a literal declared culture ships as a cultureDateParse node; a standard specifier / 2-digit year / dynamic pattern\|culture stays server |
| `DateTime.Today` | client | — |
| `DateTime.UtcNow` | client | — |
| `DateTimeOffset.UtcNow` | client | — |
| `DurableClock.Now` | client | — |
| `DurableClock.Today` | client | — |
| `DurableClock.UtcNow` | client | — |
| `Enum.Description` | client | model-backed; ships as std:Enum.Description, dispatched by the client's dedicated arm |
| `Enum.Label` | client | model-backed; ships as std:Enum.Label, dispatched by the client's dedicated arm over the enum map |
| `Enum.Name` | client | model-backed; ships as std:Enum.Name, dispatched by the client's dedicated arm |
| `Enumerable.Range` | client | — |
| `File.SignedUrl` | server | needs the session's app + the signing key to mint a signed URL — never leaves the server |
| `File.Url` | client | ships as fileUrl — the render evaluator builds the URL in-process |
| `Guid.Empty` | client | — |
| `Guid.NewGuid` | client | — |
| `Guid.Parse` | client | — |
| `JsonSerializer.Serialize` | conditional | gated by CanJsonSerializeOnClient — a scalar or list of scalars ships; a class or entity needs the server's model |
| `Math.Abs` | client | — |
| `Math.Acos` | client | — |
| `Math.Asin` | client | — |
| `Math.Atan` | client | — |
| `Math.Atan2` | client | takes **(y, x)** — C#'s order. `Atan(y / x)` is not the same: it loses the sign and folds two quadrants onto two others |
| `Math.Ceil` | client | — |
| `Math.Ceiling` | client | — |
| `Math.Clamp` | client | — |
| `Math.Cos` | client | radians |
| `Math.Exp` | client | — |
| `Math.Floor` | client | — |
| `Math.Log` | client | — |
| `Math.Log10` | client | — |
| `Math.Log2` | client | — |
| `Math.Max` | client | — |
| `Math.Min` | client | — |
| `Math.Pow` | client | — |
| `Math.Round` | client | — |
| `Math.Sign` | client | — |
| `Math.Sin` | client | radians |
| `Math.Sqrt` | client | — |
| `Math.Tan` | client | radians |
| `Math.Truncate` | client | — |
| `Regex.IsMatch` | client | ships as regexIsMatch, with the pattern translated to JS's dialect at compile time |
| `Regex.Replace` | client | ships as regexReplace, with the pattern translated to JS's dialect at compile time |
| `Regex.Split` | client | ships as regexSplit, with the pattern translated to JS's dialect at compile time |
| `Security.HashPassword` | server | password hashing runs where the host's cost factor and salt RNG live |
| `Security.IssueJwt` | server | a JWT is signed with the host's key; a client-minted token would be self-issued authority |
| `Security.LinkOAuthFromPending` | server | it opens a server-sealed pending-oauth token and writes the identity link; the verified subject never crosses to the client |
| `Security.RandomHex` | server | an unguessable value must come from the host's RNG, not the user's machine |
| `Security.RandomId` | server | an unguessable id must come from the host's RNG, not the user's machine |
| `Security.VerifyPassword` | server | password verification runs where the hash does — the client never sees a hash |
| `Security.VerifyPendingOAuthEmail` | server | it opens a server-sealed pending-oauth token; the callback-verified email re-enters only inside the seal, never as a client argument |
| `Text.ByteSize` | client | — |
| `Text.Capitalize` | client | — |
| `Text.Concat` | client | — |
| `Text.Contains` | client | — |
| `Text.EndsWith` | client | — |
| `Text.IndexOf` | client | — |
| `Text.IsBlank` | client | — |
| `Text.IsEmpty` | client | — |
| `Text.Join` | client | — |
| `Text.LastIndexOf` | client | — |
| `Text.Left` | client | — |
| `Text.Length` | client | — |
| `Text.Like` | client | — |
| `Text.Lower` | client | — |
| `Text.PadEnd` | client | — |
| `Text.PadStart` | client | — |
| `Text.Repeat` | client | — |
| `Text.Replace` | client | — |
| `Text.Reverse` | client | — |
| `Text.Right` | client | — |
| `Text.Split` | client | — |
| `Text.StartsWith` | client | — |
| `Text.Substring` | client | — |
| `Text.TitleCase` | client | — |
| `Text.Trim` | client | — |
| `Text.TrimEnd` | client | — |
| `Text.TrimStart` | client | — |
| `Text.Truncate` | client | — |
| `Text.Upper` | client | — |
| `TimeOnly.FromDateTime` | client | — |
| `TimeOnly.New` | client | — |
| `TimeOnly.Parse` | client | — |
| `TimeSpan.Days` | client | — |
| `TimeSpan.FromDays` | client | — |
| `TimeSpan.FromHours` | client | — |
| `TimeSpan.FromMilliseconds` | client | — |
| `TimeSpan.FromMinutes` | client | — |
| `TimeSpan.FromSeconds` | client | — |
| `TimeSpan.Hours` | client | — |
| `TimeSpan.Minutes` | client | — |
| `TimeSpan.New` | client | — |
| `TimeSpan.Parse` | client | — |
| `TimeSpan.Seconds` | client | — |
| `TimeSpan.TotalDays` | client | — |
| `TimeSpan.TotalHours` | client | — |
| `TimeSpan.TotalMilliseconds` | client | — |
| `TimeSpan.TotalMinutes` | client | — |
| `TimeSpan.TotalSeconds` | client | — |
| `TimeSpan.Zero` | client | — |
| `Uri.EscapeDataString` | client | — |
| `Uri.UnescapeDataString` | client | — |
| `WebUtility.HtmlDecode` | client | — |
| `WebUtility.HtmlEncode` | client | — |
| `Zone.InZone` | conditional | ships for a LITERAL declared zone (client has its DST plan); a dynamic zone → server |
| `Zone.IsDst` | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
| `Zone.New` | server | the zone factory is a compile-time literal fold — no runtime work to accelerate |
| `Zone.OffsetAt` | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
| `Zone.Resolve` | conditional | gated per call site — a literal (declared) zone ships as a zoneResolve node; a dynamic zone stays server |
| `string.Concat` | client | — |
| `string.Join` | client | — |

## See also        {#see-also}
- <span class="planned" title="this page is planned and not written yet">stdlib-text</span> · [Regex](https://osysharp.com/reference/stdlib/regex/) · [Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) · [Uri](https://osysharp.com/reference/stdlib/uri/) — the individual surfaces
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — the server-only authority surface, and why it is
