Documentation / Collections and maps

Collections and maps

Collections keep values in order. Maps keep values under keys and remember insertion order too.

Both use braces because square brackets already have a full-time job building templates.

One historical note: a fruit example still spells strawberry with one r to confuse old LLMs. Tradition matters.

Choose the explanation level: Collection literals

Collection literals

Collections use curly braces and keep their values in order.


    scores = {10, 20, 30}
    names = {"Priya", "Grace"}
    

Every item has the same element type.

An empty collection needs an explicit type:


    scores ~{Int} = {}
    

Collections start at index 0.

Use collection methods such as get, set, push and remove instead of square-bracket indexing.

Collections form ordered, zero-indexed, homogeneous groups.

Collection types and literals use braces.


    values ~= {1, 2, 3}
    names {String} = {"Priya", "Grace"}
    

Literal inference

  • A non-empty literal infers one element type from its items.
  • Every item must be compatible with that element type.
  • A typed collection receiving site may apply normal contextual coercion to each item.
  • A mixed literal such as {1, "bad"} is invalid.
  • The element type is fixed when the literal is created.

An empty literal needs an explicit collection type at its declaration or field default site.


    values ~{Int} = {}
    

The compiler does not infer an empty collection from:

  • a later push
  • later assignment
  • a loop
  • a function argument
  • HIR use
  • borrow analysis

Collection versus map literals

A top-level key/value entry with = makes a non-empty brace literal a map literal.

Every entry in that literal must then be a key/value pair.

Collection items and map entries cannot be mixed.

Syntax boundaries

  • Collections do not use square-bracket indexing.
  • Square brackets belong to templates.
  • Element reads and writes use compiler-owned collection members.
  • ~ is binding or access mode. It is not part of collection type identity.
Choose the explanation level: Growable collections

Growable collections

A growable collection can gain and lose items.


    names ~{String} = {}

    ~names.push("Priya") catch:
        assert(false, "push failed")
    ;
    

The binding is mutable because the collection changes.

An immutable collection can still be read:


    names = {"Priya", "Grace"}

    first = names.get(0) catch:
        then "guest"
    ;
    

{T} is a growable collection of T.


    names ~{String} = {}
    values {Int} = {1, 2, 3}
    

Contract

  • The element type is part of semantic type identity.
  • A growable {Int} is not compatible with {String}.
  • Growable and fixed collections are different types.
  • A mutable binding is required for operations that change the collection.
  • An immutable growable collection remains readable but cannot be mutated through that binding.
  • length() returns the current logical number of items.
  • The element type is not widened by later mutation.

push remains fallible and must be handled even for a growable collection. Runtime allocation and backend failures must not become unchecked language behaviour.


    names ~{String} = {}

    ~names.push("Priya") catch:
        assert(false, "growable push failed")
    ;
    

Use Mutable bindings for the distinction between mutable storage and one exclusive operation.

Choose the explanation level: Fixed collections

Fixed collections

A fixed collection has a maximum number of items in its type.


    values {3 Int} = {10, 20}
    

This value has length 2 and capacity 3.

A compile-time constant can name the capacity:


    capacity #Int = 4
    names ~{capacity String} = {}
    

Different capacities create different types.

A {4 Int} does not silently become {8 Int} or growable {Int}.

{N T} is a fixed collection of T with maximum length N.


    values {3 Int} = {10, 20}

    capacity #Int = 4
    scratch ~{capacity String} = {}
    

Type identity

Fixed capacity is semantic type identity.

These are distinct incompatible types:


        {Int}
        {4 Int}
        {8 Int}
    

Capacity is not a growable allocation hint.

There is no implicit conversion:

  • from growable to fixed
  • from fixed to growable
  • between different fixed capacities

Capacity syntax

Capacity in type position must be:

  • a positive Int literal
  • a bare visible #Int constant name

Capacity position is not general expression position.

Arithmetic, calls, field access, const-record projection, conditionals and nested expressions are invalid there.

Name the calculation first:


    base #Int = 4
    larger_capacity #Int = base + 2
    values ~{larger_capacity String} = {}
    

Capacity-only shorthand

A binding declaration with an immediate non-empty literal may infer the element type:


    capacity #Int = 4
    labels {capacity} = {"alpha", "beta"}
    

{capacity} shorthand is invalid for:

  • empty literals
  • non-literal initializers
  • function signatures
  • aliases
  • fields
  • return slots

Empty fixed values

An immutable binding cannot be initialised with an empty fixed literal because there is no later mutable owner path to fill it.

These are valid:

  • a mutable empty fixed binding
  • a fixed collection field default with an explicit field type

A fixed literal may contain fewer items than its capacity.

Its logical length is the number of stored items, not the maximum capacity.

Choose the explanation level: Collection operations

Collection operations

Read an item with get:


    first = items.get(0) catch:
        then 0
    ;
    

Change a stored item with set:


    ~items.set(0, 99) catch:
    ;
    

Add and remove items with push and remove:


    ~items.push(40) catch:
    ;

    removed = ~items.remove(1) catch:
        then 0
    ;
    

Get the current number of items with length():


    count = items.length()
    

Operations that can fail must use ! or catch.

Collections use five compiler-owned member operations.

get


    value = items.get(index) catch:
        then fallback
    ;
    
  • argument: index Int
  • success: the element value
  • failure: builtin Error!
  • receiver access: shared

set


    ~items.set(index, value) catch:
    ;
    
  • arguments: index Int, then one element value
  • success: no value
  • failure: builtin Error!
  • receiver access: mutable
  • replaces an existing element only
  • does not fill unused fixed capacity

push


    ~items.push(value) catch:
    ;
    
  • argument: one element value
  • success: no value
  • failure: builtin Error!
  • receiver access: mutable
  • appends after the current last element
  • fails when a fixed collection is full

remove


    removed = ~items.remove(index) catch:
        then fallback
    ;
    
  • argument: index Int
  • success: the removed element
  • failure: builtin Error!
  • receiver access: mutable
  • shifts later elements down
  • frees one slot in a fixed collection

length


    count = items.length()
    
  • no arguments
  • success: Int
  • infallible
  • receiver access: shared
  • returns logical length, not fixed capacity

Shared rules

  • get, set, push and remove must use postfix ! or catch.
  • set, push and remove require ~receiver.
  • Builtin member arguments are positional-only.
  • Assignment through get is removed.
  • Square-bracket indexing is not supported.
  • Invalid indices use the typed error path rather than a silent no-op.

Error propagation and recovery are specified by Errors, options and assertions.

Choose the explanation level: Hash maps

Hash maps

A hash map stores values under keys.


    scores ~= {
        "Priya" = 10,
        "Grace" = 12,
    }
    

Use get to read a value:


    score = scores.get("Priya") catch:
        then 0
    ;
    

Use set and remove through a mutable receiver:


    ~scores.set("Linus", 7) catch:
    ;

    removed = ~scores.remove("Grace") catch:
        then 0
    ;
    

Maps remember insertion order.

length is a property:


    count = scores.length
    

Builtin keys are limited to String, Int, Bool and Char.

Hash maps form insertion-ordered key/value groups.

Types and literals


    scores ~= {"Priya" = 10, "Grace" = 12}
    empty_scores ~{String = Int} = {}
    
  • A map type is {Key = Value}.
  • A map literal contains key = value entries.
  • Any top-level = entry makes a non-empty brace literal a map literal.
  • Every entry must then be a key/value pair.
  • Collection items and map entries cannot be mixed.
  • An empty map literal needs an explicit or immediate contextual map type.
  • A bare identifier in key position is a variable reference, not string shorthand.
  • Map values follow the same runtime-storable rules as collection elements.

Key contract

Builtin map keys are permanently limited to:

  • String
  • Int
  • Bool
  • Char

Invalid key families include:

  • Float
  • structs
  • choices
  • collections
  • maps
  • traits
  • functions
  • external opaque types
  • generic parameters

The builtin map surface does not expose user-defined HASHABLE, custom hashers or custom comparers.

Storage and order

Maps own their entry structure. Existing keys and values stored in entries follow the ordinary shared-reference, explicit-copy and inferred-transfer rules.

Insertion order follows these rules:

  1. First insertion chooses the entry position.
  2. Replacing an existing key updates the value without moving the entry.
  3. Replacement keeps the existing stored key.
  4. Removing a key removes its entry from the order.
  5. Re-inserting that key appends a new entry.

Operations

get(key):

  • shared receiver
  • fallible
  • returns shared access to the stored value

contains(key):

  • shared receiver
  • infallible
  • returns Bool

set(key, value):

  • mutable receiver
  • fallible
  • inserts or replaces
  • returns no old value

remove(key):

  • mutable receiver
  • fallible
  • removes the entry and returns the removed value under the normal lifetime and ownership rules

clear():

  • mutable receiver
  • infallible
  • removes every entry

length:

  • shared read-only property
  • no parentheses
  • returns Int
  • cannot be assigned

    score = scores.get("Priya") catch:
        then 0
    ;

    found = scores.contains("Priya")

    ~scores.set("Linus", 7) catch:
    ;

    removed = ~scores.remove("Grace") catch:
        then 0
    ;

    count = scores.length
    ~scores.clear()
    

get, contains and remove borrow the lookup key.

A live shared value returned by get prevents mutation of the same map until that shared access is no longer used.

Outside the builtin map surface

  • hashset syntax
  • map equality
  • indexing syntax
  • mutable entry APIs
  • const maps
  • fixed-capacity maps
  • specialized map variants
  • user-defined key types

More sophisticated maps belong in ordinary package structs.

Inline nesting limit

Map types nested more than two levels deep inline are rejected by the parser. Use a named type alias for deeper nesting.

The current builtin map runtime path is HTML-JS. Targets without map lowering reject reachable map use before backend lowering. Use the progress matrix for current target coverage.