Reference Verse compiles

Expressions Have Values: Verse's Most Powerful Idea

In most languages, `if` is a *statement* — it does something but produces nothing. In Verse, `if` is an *expression* — it evaluates to a value you can store, pass, or return. This single idea unlocks cleaner, safer, and more expressive island logic. Once it clicks, you'll never write a throwaway variable again.

Updated Examples verified on the live UEFN compiler
Watch the Knotexpressions_have_values in ~90 seconds.

Overview

A Verse expression is the smallest unit of code that produces a value when evaluated. The revolutionary part: everything in Verse is an expression. if/else, blocks delimited by indentation, arithmetic, function calls — they all hand back a value the moment they finish running.

This matters on your island because:

  • You can assign the result of an if/else directly to a variable instead of mutating a variable inside each branch.
  • You can pass an if/else as a function argument without a temporary.
  • Multi-line blocks evaluate to the value of their last expression, so you can chain setup logic and still capture a result.
  • Failable expressions (those that can fail as well as succeed) compose cleanly with if — the whole if block fails rather than crashing.

Reach for expression-oriented style whenever you find yourself writing:

var Result : int = 0
if (Condition):
    set Result = 42
else:
    set Result = 7

That pattern is valid Verse, but the expression form is shorter, clearer, and avoids a mutable variable entirely.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: Clifftop Cove — Timed Bonus Chest

You're building a sunny clifftop cove level. A glittering chest sits on the dock. When a player steps on a pressure plate, the game awards them a score bonus — double points if they arrive within the first 30 seconds of the round, normal points after that. A HUD message reflects which tier they earned.

The entire bonus-tier decision is handled with a single if/else expression assigned to a constant. No mutable temporaries, no duplicated grant calls.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

# Localized message helper — message is a special type, not a plain string.
EarnedMessage<localizes>(S : string) : message = "{S}"

clifftop_bonus_device := class(creative_device):

    # Drag a trigger_device onto the dock pressure plate in the editor.
    @editable
    DockPlate : trigger_device = trigger_device{}

    # Drag a hud_message_device into the editor for on-screen feedback.
    @editable
    HudDisplay : hud_message_device = hud_message_device{}

    # Drag an item_granter_device into the editor (configured to grant score/gold).
    @editable
    BonusGranter : item_granter_device = item_granter_device{}

    # How many seconds count as "early arrival" for the double-bonus window.
    @editable
    EarlyWindowSeconds : float = 30.0

    # Tracks elapsed time since the round began.
    var ElapsedSeconds : float = 0.0

    OnBegin<override>()<suspends> : void =
        # Subscribe to the plate — handler fires when any player steps on it.
        DockPlate.TriggeredEvent.Subscribe(OnPlateTriggered)

        # Tick elapsed time every second using a loop expression.
        # The loop itself is an expression (it evaluates to false when it exits,
        # but here we run it for its side-effects).
        loop:
            Sleep(1.0)
            set ElapsedSeconds += 1.0

    # Event handler — listenable(?agent) passes an optional agent.
    OnPlateTriggered(MaybeAgent : ?agent) : void =
        # Unwrap the optional agent before using it.
        if (A := MaybeAgent?):
            # ---- THE CORE LESSON ----
            # if/else is an EXPRESSION. It evaluates to one of two strings.
            # We assign that value directly — no mutable temp needed.
            BonusTier := if (ElapsedSeconds <= EarlyWindowSeconds):
                "DOUBLE BONUS — Early Arrival!"
            else:
                "Bonus Chest Claimed!"

            # Pass the expression result straight into the localized helper.
            HudDisplay.SetText(EarnedMessage(BonusTier))

            # Grant the item — the granter handles the actual reward.
            BonusGranter.GrantItem(A)

Line-by-line highlights

Line What it teaches
BonusTier := if (...): "DOUBLE BONUS..." else: "Bonus Chest Claimed!" if/else is an expression. Both branches are string literals; the whole thing evaluates to one string value.
HudDisplay.SetText(EarnedMessage(BonusTier)) The result is used immediately as a function argument — no intermediate variable.
set ElapsedSeconds += 1.0 += is also an expression (it evaluates to the new value), used here for its side-effect.
if (A := MaybeAgent?) Failable expression: MaybeAgent? either succeeds (unwrapping the agent) or fails (skipping the block).
loop: Sleep(1.0) ... loop is an expression too — it evaluates to false when it exits via break.

Common Patterns

Pattern 1 — Block expressions evaluate to their last line

A multi-line indented block is itself an expression. Its value is whatever the last expression in the block evaluates to. Use this to do setup work and still capture a result.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

block_value_device := class(creative_device):

    @editable
    ScoreTrigger : trigger_device = trigger_device{}

    @editable
    RewardGranter : item_granter_device = item_granter_device{}

    # Player's current streak — affects the multiplier.
    var Streak : int = 0

    OnBegin<override>()<suspends> : void =
        ScoreTrigger.TriggeredEvent.Subscribe(OnScored)
        # Keep the device alive.
        Sleep(Inf)

    OnScored(MaybeAgent : ?agent) : void =
        if (A := MaybeAgent?):
            # A block expression: the last line (the int) is the value.
            # All the lines before it are setup side-effects.
            Multiplier :=
                set Streak += 1
                # Give a bigger multiplier every 5 hits.
                if (Mod[Streak, 5] = 0):
                    3   # triple points on every 5th hit
                else:
                    1

            # Multiplier is now an int we can use.
            if (Multiplier > 1):
                # Grant a bonus item on multiplier rounds.
                RewardGranter.GrantItem(A)

Key idea: The block assigned to Multiplier first increments Streak (side-effect), then evaluates the if/else expression as its last line. That integer — 3 or 1 — becomes the value of the whole block.


Pattern 2 — Failable expressions compose with if

Some expressions can fail (like array indexing or option unwrapping). Because they're expressions, you compose them inside if — the whole block is skipped on failure instead of crashing.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

shore_podium_device := class(creative_device):

    # Three podium triggers along the shore — first, second, third place.
    @editable
    PodiumTriggers : []trigger_device = array{}

    @editable
    PrizeGranter : item_granter_device = item_granter_device{}

    OnBegin<override>()<suspends> : void =
        # Subscribe to each podium trigger.
        for (T : PodiumTriggers):
            T.TriggeredEvent.Subscribe(OnPodiumReached)
        Sleep(Inf)

    OnPodiumReached(MaybeAgent : ?agent) : void =
        # Two failable expressions composed in one if:
        #   1. MaybeAgent? — unwrap the option (fails if no agent)
        #   2. PodiumTriggers[0] — array index (fails if array is empty)
        # If EITHER fails, the whole block is skipped safely.
        if:
            A := MaybeAgent?
            FirstPodium := PodiumTriggers[0]
        then:
            # We only reach here if both succeeded.
            PrizeGranter.GrantItem(A)

Key idea: MaybeAgent? and PodiumTriggers[0] are both failable expressions. Composing them inside a single if means the body only runs when all of them succeed — no nested null-checks, no try/catch.


Pattern 3 — Arithmetic and comparison expressions as values

Arithmetic operators produce values just like if/else does. You can nest them, store them, or pass them directly.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

CoveScoreMessage<localizes>(S : string) : message = "{S}"

cove_score_device := class(creative_device):

    @editable
    CollectTrigger : trigger_device = trigger_device{}

    @editable
    ScoreHud : hud_message_device = hud_message_device{}

    var BaseScore : int = 100
    var BonusScore : int = 50

    OnBegin<override>()<suspends> : void =
        CollectTrigger.TriggeredEvent.Subscribe(OnCollect)
        Sleep(Inf)

    OnCollect(MaybeAgent : ?agent) : void =
        if (MaybeAgent?):
            # Arithmetic expression used directly as an argument.
            # BaseScore + BonusScore evaluates to an int value inline.
            TotalScore := BaseScore + BonusScore

            # Comparison expression: > evaluates to logic (true/false),
            # used directly inside if without a temp bool variable.
            ResultLabel := if (TotalScore > 100):
                "Cove Champion!"
            else:
                "Good run!"

            ScoreHud.SetText(CoveScoreMessage(ResultLabel))

Key idea: BaseScore + BonusScore is an expression that evaluates to int. TotalScore > 100 is an expression that evaluates to a logic value. Neither needs a named temporary — both can be used exactly where their value is needed.


Gotchas

1. Both branches of if/else must produce the same type

When you use if/else as an expression to assign a value, both branches must evaluate to the same type. Mixing int and string in the two branches is a compile error.

# ❌ WRONG — type mismatch
Result := if (Condition): 42 else: "forty-two"

# ✅ CORRECT — both branches are string
Result := if (Condition): "42" else: "7"

2. A standalone if (no else) is failable, not a value expression

An if without an else can fail — it doesn't always produce a value. You cannot assign it to a plain variable. Use it inside another if block or add an else branch.

# ❌ WRONG — if without else can fail, can't assign to non-option
Label := if (Score > 10): "High"

# ✅ CORRECT — always has a value
Label := if (Score > 10): "High" else: "Low"

3. message is not string — use a localizes helper

Device methods like hud_message_device.SetText() take a message, not a string. You cannot pass a raw string literal or use a fictional StringToMessage. Declare a <localizes> helper function:

MyMsg<localizes>(S : string) : message = "{S}"
# Then: HudDisplay.SetText(MyMsg("Hello cove!"))

4. int and float do not auto-convert

Verse never silently converts between numeric types. If your expression produces an int and you need a float, use an explicit conversion or write a float literal (30.0 not 30).

# ❌ WRONG — ElapsedSeconds is float, EarlyWindowSeconds must also be float
var EarlyWindowSeconds : int = 30
if (ElapsedSeconds <= EarlyWindowSeconds): ...

# ✅ CORRECT
var EarlyWindowSeconds : float = 30.0

5. The value of a loop is false, not the last iteration's value

loop is an expression, but it evaluates to false (logic) when it exits via break. Don't try to capture a meaningful value from a loop expression — use a var that you set inside the loop instead.

6. Block expressions: only the last line is the value

If you want a block to produce a value, make sure the value-producing expression is the last line. Any expression that isn't last is evaluated purely for side-effects, and its value is discarded.

Build your own lesson with expressions_have_values

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →