Version: dev

Constants and conditionals

ifelse, Pre, const, true, enum, and the niladic constants.

ifelse

Arity 3: ifelse(condition, then, else). The condition follows the usual convention — zero is false, non-zero is true — and the result is then when it holds, else otherwise.

Text

ifelse(a >= 0, sqrt(a), 0)

JSON

{
  "op": "ifelse",
  "args": [
    { "op": ">=", "args": ["a", 0] },
    { "op": "sqrt", "args": ["a"] },
    0
  ]
}

With a=9 → 3.0. With a=-1 → 0.0.

Laziness is not guaranteed. The scalar evaluators walk only the taken branch, so the example above is safe on a scalar. The array evaluators are eager by construction: they broadcast over lanes, and per-lane laziness would need masked evaluation. Inside an faq both branches are evaluated, so ifelse will not protect sqrt from a negative input. This is a deliberate, documented divergence between the scalar and array paths — write the guard into the argument instead:

Text

sqrt(max(a, 0))

JSON

{ "op": "sqrt", "args": [{ "op": "max", "args": ["a", 0] }] }

ifelse on an index symbol is also how a stencil’s weights are written without enumerating them — see the second difference on the aggregation page.

Pre

Arity 1. Used inside event affects: Pre(x) is the value x had before the event fired, so an affect can reference the pre-event state while assigning the post-event one. It has no meaning outside an event context. See coupling and events .

Text

Pre(x)

JSON

{ "op": "Pre", "args": ["x"] }

true

The nullary boolean literal. args must be empty. Its use is as an always-true predicate — the body of an index-set-producing faq , or a join gate that admits everything.

Text

true

JSON

{ "op": "true", "args": [] }

There is no false operator; write 0.

const

args MUST be empty; the value lives in a value field, which may be any JSON value — a number, an integer, or a nested array.

Text

[0, 0.5, 1]

JSON

{ "op": "const", "value": [0.0, 0.5, 1.0], "args": [] }

const exists for AST positions that need a literal array where a bare number will not do: an index lookup table, an interp.searchsorted query vector, a small coefficient set, an in-file polygon ring. A scalar literal does not need it — just write the number.

Keep these small. A large table belongs in a data_sources entry, not inline in the expression tree.

enum

Resolves a symbol from the document’s enums registry to its integer, so a categorical lookup reads as a name rather than a magic number. args is a two-element array of string literals, not sub-expressions: [enum_name, symbol].

{ "op": "enum", "args": ["land_use_class", "water"] }

It is lowered at load to the integer, exactly as if you had written {"op": "const", "value": 9}. An unknown enum or symbol is rejected at load with unknown_enum / unknown_enum_symbol.

enum has no distinct text form: it prints as a dotted name, which re-parses as a scoped variable reference.

The idiomatic use is a categorical index into a tabulated array:

{
  "op": "index",
  "args": [
    "r_c_table",
    { "op": "enum", "args": ["land_use_class", "deciduous_forest"] },
    { "op": "enum", "args": ["season", "summer"] }
  ]
}

In a whole document:

{
  "esm": "1.0.0",
  "metadata": { "name": "EnumExample" },
  "enums": {
    "land_use_class": { "urban": 1, "grassland": 2, "water": 3 }
  },
  "models": {
    "Surface": {
      "variables": {
        "z0": { "type": "unknown", "units": "m" }
      },
      "equations": [
        { "lhs": "z0",
          "rhs": { "op": "index",
                   "args": [
                     { "op": "const", "value": [1.0, 0.03, 0.0001], "args": [] },
                     { "op": "enum", "args": ["land_use_class", "water"] }
                   ] } }
      ]
    }
  }
}

Enum values are file-local: two documents may number the same symbol differently, and nothing outside the file may depend on the integer.

Niladic constants — do not use

The Julia evaluator’s dispatch table carries pi, π, e, true, and false as zero-arity operators. Of these, only true is specified: the other four appear in no operator table in esm-spec.md, in no schema list, and nowhere in the conformance corpus — so nothing holds the bindings to a shared meaning, and they do not agree. Python’s expression parser reads pi, e, and false as ordinary variable references rather than operators; the Rust operator registry asserts that false is not a registered operator at all, alongside pow, **, power, and =.

Write the numeric literal instead — 3.141592653589793 for π, 0 for false. For a true predicate use true, which is specified and is described above.