Documentation / Values and bindings

Values and bindings

Names cost little. Copies aren't automatic. Existing values share by default, mutability stays explicit and copy draws the line between another view and an independent value.

These rules apply to every type. The memory-management pages explain how the compiler validates and lowers them.

Choose the explanation level: Bindings

Bindings

A binding names a value.


    score = 10
    player_name = "Priya"
    

Moth usually infers the type from the value.

Write the type when it makes the code clearer or gives the compiler needed context:


    score Int = 10
    player_name String = "Priya"
    

Bindings stay immutable unless you mark them as mutable. An immutable binding can't take a different value later.

A binding gives a value a visible name.


    count = 42
    ratio Float = 0.5
    

Declaration forms


    name = value
    name Type = value
    
  • A binding must have an initializer.
  • Without an annotation, the initializer supplies the natural type.
  • An explicit annotation supplies a receiving type and may apply normal contextual coercion.
  • Ordinary bindings are immutable by default.
  • = creates a binding when the name is new.
  • = reassigns an existing binding only when that binding is mutable.

Compile-time constants use # and are documented separately by Constants and compile-time behaviour.

Binding mode and type

Binding mode is not semantic type identity.

An immutable String binding and a mutable String binding still have the same semantic String type. The binding decides which later operations are permitted.

Scope

A binding is visible in its scope and child scopes unless a language rule creates a narrower visibility boundary.

Moth does not allow a visible name to be redeclared. See the Shadowing section for the exact rule.

Choose the explanation level: Mutable bindings

Mutable bindings

Most bindings should stay immutable. Add ~ when the value needs to change.


    count ~= 0
    count = 1
    count += 1
    

The ~ appears only when the binding is declared. Later assignments use normal =.

Collections also need a mutable binding before they can change:


    names ~{String} = {}

    ~names.push("Priya") catch:
    ;
    

The ~ before names.push requests exclusive access for that call. It is separate from the ~ that made the binding mutable.

~ on a declaration marks the binding as mutation-capable.


    count ~= 0
    names ~{String} = {}
    

Declaration and reassignment


    count ~= 0
    count = 1
    
  • name ~= value declares a mutable binding with an inferred type.
  • name ~Type = value declares a mutable binding with an explicit type.
  • Reassignment still uses ordinary =.
  • ~= is an adjacent mutable-declaration marker. Write whitespace before ~ and after =, but never between them.
  • Mutability belongs to the binding, not the semantic type.

Exclusive access

Declaration mutability and call-site exclusive access are separate operations.


    add_name |names ~{String}, name String|:
        ~names.push(name) catch:
        ;
    ;

    names ~{String} = {}
    add_name(~names, "Priya")
    

A mutable parameter accepts:

  • ~place for an existing mutable place
  • a plain fresh rvalue such as a literal, template, constructor call or computed value

~ is place-only syntax. It is invalid on immutable bindings and is not written on fresh rvalues.

~ requests exclusive access for that operation. It is not explicit move syntax and is not part of type identity.

Choose the explanation level: Shared access

Shared access

A new name for an existing value doesn't automatically copy that value.


    names = {"Priya", "Grace"}
    same_names = names
    

same_names is another read-only view of the existing value.

This keeps ordinary code cheap. You can pass values around without copying them first.

You can't use shared access to change the value. Mutation needs a mutable binding and an explicit exclusive operation.

Existing values use shared read-only access by default.

Binding an existing value to another name doesn't silently copy it.


    items ~= {"a", "b"}
    shared_items = items
    

Contract

  • Any number of shared accesses may coexist.
  • Shared access is read-only.
  • Shared access doesn't imply ownership transfer.
  • The rule applies uniformly to stack and heap values.
  • Moth offers no source & or &mut operators.
  • A backend may optimise representation, but observable source semantics must stay the same.

Binding mutability is separate from access. A mutable binding can still be shared read-only. An exclusive operation must satisfy the borrow rules before it can proceed.

Alias conflicts

A later use of a shared alias can prevent overlapping mutation:


    items ~= {"a", "b"}
    shared_items = items

    -- Invalid because shared_items is used again after the mutation.
    ~items.push("c") catch:
    ;

    io.line([: [shared_items.length()] items])
    

Use copy when the second name needs an independent value.

Choose the explanation level: Explicit copies

Explicit copies

Use copy when two stored values need to change independently.


    names ~= {"Priya", "Grace"}
    copied_names ~= copy names

    ~names.push("Linus") catch:
    ;
    

Changing names does not add "Linus" to copied_names.

This is different from a normal assignment:


    shared_names = names
    

A normal assignment creates another shared view. copy creates an independent value.

copy works on a stored name or field:


    copied_title = copy page.title
    

Do not apply copy directly to literals, constructor calls or computed expressions; copy accepts places only. Those expressions create new result roots, but existing values stored inside them still follow ordinary shared-reference rules.

copy requests an independent value from an existing place.


    items ~= {"a", "b"}
    independent_items ~= copy items

    ~items.push("c") catch:
    ;

    io.line([: original length: [items.length()]])
    io.line([: copied length: [independent_items.length()]])
    

Copy targets

The operand must be a place with stable storage:

  • a visible value binding
  • a field projection such as copy config.title
  • a parenthesised place such as copy (items)

Literals, templates, constructor calls, function calls and computed expressions are not copy targets.


    copied_title = copy config.title
    copied_items = copy (items)
    

These forms are invalid:


    copied_number = copy 42
    copied_result = copy make_value()
    copied_template = copy [: text]
    

Fresh expressions already create their own result values.

Contract

  • Source code does not silently copy an existing value when another name is created.
  • When a stored value is copyable and an independent value is required, write copy.
  • The copied value keeps the same semantic type.
  • The receiving binding chooses its own mutability.
  • Copying is separate from ownership transfer and separate from exclusive access.

Expressions that construct a new result from existing inputs are not implicit copies of one input.


    total = unit_price + shipping
    label = [: [first_name] [last_name]]
    

Those expressions create new result values through their own language rules. The first computes a numeric sum. The second builds a string through a template.

Choose the explanation level: Shadowing

Shadowing

A visible name can't be declared again.


    score = 10

    if score > 0:
        -- Invalid: score already exists.
        score Int = 20
    ;
    

Choose a different name instead:


    score = 10

    if score > 0:
        doubled_score = score * 2
    ;
    

Reassigning a mutable binding is different. That changes the existing binding instead of creating another one.

Moth doesn't allow a visible name to be redeclared.


    ready = true
    label = "outer"

    if ready:
        -- Invalid: label is already visible.
        label String = "inner"
    ;
    

Contract

  • A declaration can't reuse a value name that stays visible.
  • A declaration can't collide with a visible type, alias, import, builtin or reserved name.
  • Function parameters and loop bindings follow the same rule.
  • A nested child scope can't shadow a name from its parent scope.
  • Assignment to an existing mutable binding is not a redeclaration.
  • Separate scopes may reuse a name when neither declaration is visible from the other.

    first || -> Int:
        value = 1
        return value
    ;

    second || -> Int:
        value = 2
        return value
    ;
    

Import, module public-surface and namespace collisions add more specific rules, but they build on the same no-shadowing principle.

Related design

See Memory management for borrow validation, ownership analysis and backend lowering.