A hash map stores values under keys.
scores ~= {
"Priya" = 10,
"Rob" = 12,
}
Use get to read a value:
score = scores.get("Priya") catch:
then 0
;
Use set and remove through a mutable receiver:
~scores.set("Emmy", 7) catch:
;
removed = ~scores.remove("Rob") 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, "Rob" = 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:
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.
Every String key uses content equality and content hashing. Quoted and template-produced strings follow the same key rule because construction origin does not create a second string category.
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:
- First insertion chooses the entry position.
- Replacing an existing key updates the value without moving the entry.
- Replacement keeps the existing stored key.
- Removing a key removes its entry from the order.
- 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("Emmy", 7) catch:
;
removed = ~scores.remove("Rob") 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.
Compiler-known retention effects
Builtin maps are part of Moth's trusted dynamic-storage substrate. The compiler knows the retained-edge summary contributed by every stored key and value. A scalar can contribute zero obligations, a direct heap-backed value can contribute one, and an inline aggregate that physically stores several handles can contribute several direct obligations.
A summary may also describe nested retention, so the compiler knows what a stored value structurally contains. That is a description, not a count. Obligations are the direct edges between final allocation families: a separately allocated child owns its own outgoing edges, and an allocation reachable only through such a child is never counted again by the map. If layout refinement instead packs a child inline into the map's storage, its handles become direct map edges and do count.
The table describes each operation's successful path. Maps retain both stored keys and stored values. Replacing an existing key keeps the stored key and changes only the stored value. The lookup key remains a temporary borrowed alias unless the operation inserts it as a new stored key.
| Operation | Retained-edge effect | |---|---| | get or contains | creates a temporary lookup alias and adds no persistent obligation | | new-key set | adds the stored key's obligations and the new value's obligations | | existing-key set | keeps the existing stored key, removes the old value's obligations and adds the new value's obligations. The incoming lookup key is not retained | | remove | removes the stored key's and stored value's obligations. The value becomes a detached stored result, but the stored key does not | | clear | removes all stored key and value obligations | | map destruction | removes all stored key and value obligations and destroys the backing-storage domain |
For the initial direct-handle implementation, a stored scalar commonly contributes zero obligations and a direct heap-backed value commonly contributes one. The general summary supports aggregates and nested retention without a new map vocabulary.
Successful commits and failed operations
A builtin map mutation commits its retained-edge effects atomically on the successful path. A failed operation preserves the original entry structure and all cleanup obligations. A failed lookup or remove destroys no obligation. A failed insertion retains neither the incoming key nor the incoming value. A failed replacement leaves the stored key and old value obligations intact. Count changes happen after the operation reaches its semantic commit point.
Public summaries preserve the exit-specific effects:
```text success: remove old value obligations when present add new key and value obligations when inserting
error: no retention change ```
Consider a map whose key and value point to the same allocation family:
text = [: large value]
values ~{String = String} = {}
~values.set(text, text)!
removed = ~values.remove("large value")!
The new-key insertion creates two direct persistent obligations to the same allocation family, one from the stored key and one from the stored value. Because this was the final use of text, its existing cleanup obligation can be reused for exactly one of those two stored references. The second is a genuinely new obligation.
Existing-key replacement keeps the stored key, so an equal-content lookup key does not add another obligation. The lookup string passed to remove is a temporary borrowed alias, not an obligation to the stored family.
Removal drops the stored key obligation and reclassifies the stored value obligation into the result when affine responsibility can move there. One removed reference can become the result's obligation; the other simply disappears.
clear kills the map's complete retention domain. If no other live alias or retention domain can retain a target allocation family, the call becomes that family's final cleanup frontier. The map itself may continue to live. Declared-region-owned allocation storage still remains until declared-region exit.
Future map APIs must preserve narrow, analysable destruction effects. The full contract is explained for users in Automatic cleanup and retained
edges. Compiler implementation details live in Retained Edge Counting.
Outside the builtin map surface
- hashset syntax
- map equality
- indexing syntax
- mutable entry APIs
- const maps
- fixed-capacity maps
- specialised 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.