# 9. Expressions, Operators, and Readable Sugar

> See how FlowScript operators, ternaries, template literals, and struct field access lower to visible catalog nodes without hiding configuration or data flow.

- **Document type:** Book chapter
- **Canonical HTML:** [https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/)
- **Markdown alternate:** [https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/index.md](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/index.md)
- **Book:** FlowBook — The FlowScript Book
- **Edition:** Open edition · 2026
- **Publisher:** Flow-Like
- **Language:** en
- **Topics:** FlowScript expressions, operators, ternaries, template literals, struct field access
- **Chapter:** 9
- **Part:** Part II — Thinking and Writing in Flows
- **LLM index:** [https://book.flow-like.com/llms.txt](https://book.flow-like.com/llms.txt)

---

FlowScript is at its most familiar when the nodes seem to disappear:

```flow
let urgent = production && (attempts >= 3)
let status = urgent ? "critical" : "investigate"
let summary = `${status}: ${report}`
incident.status = status
```

This looks like ordinary code. It is also a compact description of a graph containing an Integer
Greater Than or Equal node, Boolean And, Select, String Format, and Set Field. Apply does not pass
these expressions to a hidden JavaScript engine. It resolves each one against the catalog and
creates or updates the corresponding nodes, pins, values, and wires.

That distinction gives us the rule for every convenience in this chapter:

> FlowScript may make a Flow shorter to read, but it may not make the Flow less exact.

If a short form can reproduce the complete node contract, the renderer may use it. If meaningful
configuration would disappear, the explicit call remains. Readability is valuable because it
helps people reason about the system; it is not permission to conceal a decision.

> **Release check:** Arithmetic, comparison and Boolean lowering, unary `!` and `-`, the four
> compound assignments, value-selecting ternaries, template literals, and Struct field writes are
> implemented in the current repository. Mixed known operand types are rejected. The current
> diagnostic does not yet offer the two intent-specific conversion repairs proposed in Section
> 9.1. Operator and template rendering already preserve configured extra pins in the cases
> described below, but three current asymmetries remain: Float equality and inequality can render
> as operators that text cannot reconcile, Integer division's declared operator result disagrees
> with its live node output, and a Struct Get `found` output can be mistaken for field-value sugar.
> Those are implementation gaps, not part of the language contract.

## 9.1 Arithmetic, comparison, and Boolean expressions

An operator is a catalog lookup with two extra clues: the symbol and the operand type. Given:

```flow
let urgent = attempts >= 3
```

FlowScript knows that `attempts` is an Integer input and `3` is an Integer literal. It can
therefore select the Integer Greater Than or Equal node and connect its Boolean output to the next
consumer. The source spelling is compact; the Board still shows the operation that made `urgent`.

The current operator families are intentionally specific:

| Values | Short forms that currently lower to catalog nodes |
| --- | --- |
| Integer | `==`, `!=`, `>`, `>=`, `<`, `<=`, `+`, `-`, `*`, `%`, `**` |
| Float | `>`, `>=`, `<`, `<=`, `+`, `-`, `*`, `/`, `**` |
| String | `==`, `!=`, `+` |
| Boolean | `==`, `&&`, `\|\|`, `^` |

The table is narrower than the parser's vocabulary. A token being syntactically recognizable does
not prove that the active catalog contains an unambiguous node contract for it. Apply is where the
symbol, types, and live catalog meet.

The parser also recognizes `|`, but current reconciliation has no operator node mapping for it.
`===` and `!==` are accepted compatibility spellings and normalize semantically to `==` and `!=`;
FlowScript does not define a separate JavaScript-style identity comparison. Integer `/` is omitted
from the table because the current operator registry expects an Integer result while the live
Integer Divide node returns a Float. Until those declarations agree, use the explicit catalog
operation and inspect its Float output rather than relying on `int / int` sugar.

Float equality and inequality demonstrate why that matters. Comparing floating-point values
usually needs a tolerance. Flow-Like's Float Equal node exposes that tolerance as an input, so `a
== b` cannot honestly express its full decision. Use the explicit call and choose the tolerance:

```flow
const closeEnough = float::equal({
    float1: measured,
    float2: expected,
    tolerance: 0.001,
})
```

The longer form is better here. It puts an operationally meaningful assumption in the source and
on the node instead of inheriting an invisible global default.

### Precedence is familiar; canonical source is explicit

FlowScript reads binary expressions with the usual precedence groups. From low to high they are
Boolean OR, Boolean AND, `|`, then `^`, equality, ordering comparisons, addition and subtraction,
multiplication/division/modulo, and exponentiation. Exponentiation associates to the right; the
others associate to the left.

The parser can therefore understand:

```flow
let urgent = production && attempts >= 3 || manuallyEscalated
```

The canonical renderer adds parentheses around nested binary expressions:

```flow
let urgent = (production && (attempts >= 3)) || manuallyEscalated
```

That is not a semantic change. It makes the tree—and consequently the chain of operator
nodes—obvious in a review. Parentheses cost very little compared with an incident caused by two
readers remembering precedence differently.

### Types decide; FlowScript does not guess intent

The same `+` symbol can identify Integer Add, Float Add, or String Concat. FlowScript chooses only
when the operand evidence gives it one meaning. Two strings concatenate. Two integers add. Two
floats add. An Integer literal may adopt the Float family when the other operand is known to be a
Float, which makes `ratio * 2` natural. Two separately typed Integer and Float values remain
different contracts and require an explicit conversion.

Consider the deceptively small expression:

```flow
let result = "5" + 1
```

There are at least two plausible results:

- numeric addition after parsing the left side: `6`; or
- text concatenation after formatting the right side: `"51"`.

Silently inserting a conversion would make the program compile by choosing a business decision
the author never made. Current reconciliation instead reports incompatible `String` and `Integer`
operands and creates no String Concat node. Apply is atomic: a reconciliation diagnostic prevents
the edit commands from being applied, so the failed expression cannot leave half of a repair on
the Board.

The right repair depends on intent. Numeric intent can be made explicit with a typed parser and
its success output:

```flow
const { integer: parsed, success } = "5".toInt({ fallback: 0 })
if (!success) {
    // Handle invalid input as part of this Flow's domain policy.
}
let total = parsed + 1
```

Text intent can be made explicit with formatting:

```flow
let text = `5${1}`
```

The desired editor experience is: reject the ambiguous Apply, offer both repairs, preview the node
each would insert, and let the author submit a second Apply for the chosen repair. Canonical source
then rewrites to that visible choice. The editor should not choose one automatically. A conversion
can fail, truncate, select a fallback, or alter representation; its policy belongs in the Flow.

The same principle applies to `any`. An `any + 1` expression has no reliable operator family.
Narrow it through a typed interface, use a typed parser such as `string::toInt`, or place
`types::tryTransform` and consume its `success` result. Try Transform is a real catalog node whose
Generic output adapts to its typed consumer; it is not an invisible coercion rule attached to
every operator. An untyped or unconnected Try Transform output produces `null` and `success =
false`; a failed conversion is an ordinary result rather than a node error. Ignoring `success`
merely moves the eventual failure to a later typed consumer.

## 9.2 Unary and compound assignment

FlowScript supports two prefix conveniences:

```flow
let unavailable = !healthy
let debt = -balance
```

Boolean negation becomes the Boolean Not node. Unlike binary operators, canonical source will
generally show that catalog call rather than preserve `!healthy`. Numeric negation canonicalizes
to subtraction from zero:

```flow
let debt = 0 - balance
```

The reconciler then chooses Integer Subtract or Float Subtract from `balance`'s type. A negative
literal such as `-3` remains a literal; an expression such as `-balance` becomes an operation. This
small difference is visible on the Board because one needs a node and the other does not.

Four compound assignments are accepted while writing:

```flow
attempts += 1
remaining -= consumed
scale *= 2.0
scale /= 4.0
```

Canonical rendering expands them:

```flow
attempts = attempts + 1
remaining = remaining - consumed
scale = scale * 2.0
scale = scale / 4.0
```

This expansion exposes the actual model. The right side reads the current producer, the operator
creates the next value, and the name is rebound for later statements. There is no opaque CPU-style
`+=` instruction inside the graph. The compact input is accepted for convenience; the durable
text makes the read, operation, and rebind explicit.

Field compound assignment follows the same rule:

```flow
incident.attempts += 1
```

becomes:

```flow
incident.attempts = incident.attempts + 1
```

That form needs a typed numeric field. An open Struct field is Generic until the Flow proves more,
so an author may have to read and convert it explicitly. `any` does not become safe merely because
the uncertainty sits behind a dot.

## 9.3 Conditional expressions select values

A ternary is a value decision:

```flow
let status = urgent ? "critical" : "investigate"
```

It lowers to the Types Select node:

> **Workflow figure:** Boolean And result connected to Select.Condition, with the selected Result fanning out to Set Field and Format String.
>
> The ternary becomes one Select data node. Its result can feed several consumers without creating execution branches.
>
> [View the figure in the canonical HTML chapter](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/).

This is not an execution branch. At runtime the Select node evaluates its condition and reads only
the selected data input, but it exposes no alternative Execution arms. An impure operation placed
inside an expression can also need its own place in the surrounding execution chain, so a ternary
must not be used as a substitute for conditional side effects. Use a ternary for value-level
selection. Use `if` or a named execution-arm block when only one side should call an external
system, write data, or handle a failure. Chapter 10 develops that distinction through execution
paths.

The condition must resolve to a Boolean input, and the two alternatives must be usable by the
same selected output contract. With ordinary literals that is straightforward. With distinct
schemas or container shapes, an explicit common boundary is safer than erasing the disagreement
through Generic.

The renderer recognizes the current `utils_types_select` node specifically. That node presently
has only `condition`, `a`, and `b`, so the ternary reproduces its full data contract. Unlike binary
operator lowering, this path does not yet guard against future extra configured inputs. If the
Select node grows another meaningful pin, the lossless-sugar test must grow with it.

## 9.4 Template literals are String Format nodes

Template literals turn a format operation into readable prose:

```flow
let summary = `${status}: ${normalized} after ${attempts} attempt(s)`
```

Apply converts that expression into one String Format node. Static text becomes its
`format_string` input. Each interpolation becomes a dynamic input pin:

> **Workflow figure:** Format String node with dynamic status, normalized, and attempts input pins wired from Select, Trim String, and the event.
>
> Template interpolation names survive as dynamic Studio pins, so every placeholder has an inspectable producer.
>
> [View the figure in the canonical HTML chapter](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/).

Simple references keep their names. A member or output access normally uses its final segment.
More complex expressions receive stable positional names such as `arg1`. If two different
expressions want the same placeholder name, the later one receives a suffix. Repeating the same
bare reference reuses one placeholder pin, just as one graph output may fan out to several places
in a format string.

This naming is not cosmetic. Dynamic pins are part of the node. The Board needs to know which wire
supplies `{status}`, and text reconstructed from the Board must be able to produce the same set of
pins again.

Template text therefore has one deliberate edge. Literal text shaped exactly like a format
placeholder, such as `{status}`, cannot be treated as inert braces, because the underlying String
Format node would interpret it as a pin. FlowScript rejects that ambiguous template. If a
placeholder was intended, write the explicit format call and supply it; if literal braces were
intended, change the text rather than relying on an escape the node itself cannot preserve.

The renderer returns to template syntax only when the round trip is exact. It requires:

- a literal format string;
- a value or wire for every placeholder;
- no extra meaningful input pins; and
- placeholder names that would be regenerated identically from the expressions.

If one of those checks fails, the Board renders the explicit call:

```flow
string::format({
    formatString: "Incident {ticket}",
    ticket: externalId,
})
```

That is the correct fallback. A slightly longer line is preferable to a pretty template that
silently renames a dynamic pin or drops a configured value.

## 9.5 Field reads and writes are value flow

A dot can select two different graph concepts:

```flow
let found = split.found
let status = incident.status
```

The first expression may name an output pin on the node bound to `split`. The second reads a field
from a Struct. Reconciliation tries the declared node output first. If no such output exists and
the base is Struct-shaped, it lowers the expression to the appropriate Struct field-access node.
A named interface lets that access retain the field's declared type; an open Struct produces a
more generic boundary.

There is a current read-side round-trip bug to keep separate from that intended rule. Generic Get
Field exposes both `value` and `found`. Board-to-text lowering currently recognizes the node as a
member access without first checking which output is wired. A wire from `found` can therefore
render as `incident.status`, which ordinarily means the value. The safe authoring form when the
presence flag matters is an explicit call and named output until lowering checks the selected pin.

```flow
const { value: status, found } = incident.get({ field: "status" })
```

A field write is equally concrete:

```flow
incident.status = "closed"
```

It lowers to Set Field with three data inputs—`struct_in`, the literal path `"status"`, and the new
value—and a `struct_out` output. FlowScript then rebinds `incident` so later uses resolve to that
output.

It helps to see the successive values on the Board:

> **Workflow figure:** Two Set Field nodes connected in sequence, carrying successive Struct values while status and summary data feed their Value pins.
>
> Each Set Field produces a new Struct wire for the next update; existing consumers remain connected to the earlier producer.
>
> [View the figure in the canonical HTML chapter](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/).

This is the precise sense in which the source appears to mutate a Struct. A consumer wired before
the assignment keeps `incident@0`. A reference after the first write receives `incident@1`; after
the second, it receives `incident@2`. The pins on each operation carry that operation's current
runtime value. The source name is the readable binding that selects which producer subsequent
expressions use.

If the name is never used after a field write, the updated Struct is simply an unused copy. If it
is used, it is the new version. Nothing travels backward through an existing wire to change a
consumer that already received the earlier producer.

Nested literal paths follow the same model:

```flow
incident.owner.name = "Ada"
incident.affectedSystems[0].status = "degraded"
```

When the field itself is computed rather than a literal path, the compact dot assignment cannot
name it honestly. The graph keeps an explicit Set Field call instead. The renderer also uses dot
assignment only for an accumulator-shaped chain where the Set Field output becomes the next value
of the same binding. A seed operation, a cross-source update, or a wired dynamic field remains an
explicit call.

## 9.6 Sugar must round-trip honestly

The chapter's complete example applies all four kinds of readable sugar to the Incident Triage
Flow:

```flow
use log::{ info }

eventsGeneric explainIncident(payload: Struct, incident: Struct, report: string, attempts: int, production: bool) {
    const normalized = report.trim()
    let urgent = production && (attempts >= 3)
    let status = urgent ? "critical" : "investigate"
    let previousStatus = incident.status
    incident.status = status
    incident.summary = `${status}: ${normalized} after ${attempts} attempt(s)`
    info({ message: `${previousStatus} -> ${incident.status}: ${incident.summary}`, toast: false })
}
```

Read it once as code. It trims a report, classifies repeated production trouble, remembers the old
status, produces two successive Incident values, and logs the transition. Then read the same logic
as a graph:

> **Workflow figure:** Three Get Field nodes feeding previousStatus, status, and summary into the final Format String before Print Info.
>
> The final focused region completes the graph detail shown in the three earlier crops: previous and updated Struct fields converge into the last format and log call.
>
> [View the figure in the canonical HTML chapter](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/).

> **Workflow figure:** Complete Explain Incident workflow containing comparison, Boolean And, Select, Trim String, two Set Field nodes, three Get Field nodes, two Format String nodes, and Print Info.
>
> The complete readable-sugar example is an overview at page width; the focused renders expose its details. Open this image for the full-size graph.
>
> [View the figure in the canonical HTML chapter](https://book.flow-like.com/part-2/09-expressions-operators-readable-sugar/).

The exact layout is Studio's concern, but every meaningful operation in the source has a node or
a pin value behind it. Conversely, every meaningful configured input on those nodes is intended to
survive when the Board becomes text. The release gaps above identify where current lowering still
falls short of that contract.

That second direction is where lossless sugar earns its name. Current lowering checks more than a
node type:

| Short form | It is rendered only when… | Otherwise… |
| --- | --- | --- |
| `a op b` | the node is a supported operator shape, its two operands are identifiable, and any omitted trailing inputs are untouched defaults | keep the catalog call |
| `c ? a : b` | the node is the recognized Types Select contract; today it has exactly the three represented inputs | keep the catalog call |
| `` `…${x}…` `` | the literal format and complete placeholder-pin set regenerate exactly | keep `string::format(...)` |
| `record.field = value` | a literal-path Set Field is rebinding the same Struct accumulator | keep `struct::set(...)` |

String equality makes the rule tangible. With the case option untouched, a graph can render:

```flow
let same = left == right
```

When the node's `ignoreCase` input is enabled, the operator can no longer carry the contract, so
the renderer preserves the setting:

```flow
const same = left.equal({ string: right, ignoreCase: true })
```

Applying or switching views may therefore make source longer. That is not a round-trip defect.
It is the language refusing to lie about the graph.

Current Struct accumulator rendering makes that concrete in the chapter example. The first Set
Field may read back as an explicit seed alias—possibly with a generated name such as `record`—and
only the following same-accumulator update may return to `record.summary = …`. The parser fixture
above is in canonical source form, but it is not a promise of byte-identical Board readback. The
contract is the preserved graph and its configuration.

The complementary failure is equally useful. Change the example to:

```flow
let urgent = report + attempts
```

The parser can recognize the expression, but Apply cannot select one correct node family. It
reports the String/Integer contradiction at that expression and does not manufacture a conversion.
The repair is local and visible: choose formatting if this is a label, or choose a typed parse and
handle its success if this is arithmetic.

These constraints are what make familiar syntax safe in a dual-view language. Operators are
typed catalog operations. Ternaries select values rather than disguising execution branches.
Templates retain their dynamic pins. Field assignment advances a visible Struct value instead of
mutating the past. Whenever the compact notation cannot carry the entire decision, FlowScript
shows the node call.

The next chapter adds visible execution structure: `if`, named outcome arms, sequential and
parallel loops, `while`, and `return`. The same standard will apply there—short syntax is welcome
only when every path still has an honest place on the Board.

---

## Reading navigation

- [Previous: Calling the Node Library](https://book.flow-like.com/part-2/08-calling-the-node-library/index.md): Discover and call Flow-Like catalog nodes with qualified names, imports, method syntax, named output pins, generated declarations, and typed search.
- [Next: Branches, Loops, Parallelism, and Return](https://book.flow-like.com/part-2/10-branches-loops-parallelism-return/index.md): Build visible control flow with if branches, named execution arms, sequential and parallel loops, bounded while, stopping, and return boundaries.
- [Complete contents](https://book.flow-like.com/contents/index.md): Return to the full FlowBook reading plan.
