Collections & Nodes
So far, every variable we've created has held a single value — one number, one string, one boolean. Real programs deal with groups of things: a list of names, a table of scores, a set of unique IDs. Auto provides several collection types that let you store, query, and transform multiple values. In this chapter, we'll also meet node, Auto's keyword for building linked structures such as chains and trees.
In this chapter, we'll cover:
- Arrays — ordered sequences created with
[1, 2, 3]literal syntax - Objects — named-field records created with
{ name: "Alice", age: 30 } - HashMaps — key-value maps inferred from string-keyed literals
- Sets — deduplicated collections created with
{1, 2, 3} - List operations —
push,pop,contains,sort, and more - Strings as collections —
len, indexing,split,to_upper - Nodes — linked structures with
node,?Item, andnone
Arrays
An array is an ordered, indexed collection of values. You create one with square brackets and a comma-separated list of elements:
var numbers = [1, 2, 3, 4, 5]The type is inferred from the elements — since every value here is an int, numbers has type [int]. You access elements by zero-based index with numbers[0], and get the length with numbers.len(). You can iterate over an array with for item in array.
This prints:
1
5
Alice
Bob
CharlieNotice how the transpiler adapts the array concept to each language:
- Rust uses
Vec<i64>— a heap-allocated dynamic vector. Thefor name in &namesloop borrows each element. - Python keeps the native list syntax
[1, 2, 3]. Thelen()built-in replaces Auto's.len()method. - C uses a plain C array
long long numbers[]with a separateintfor the length. Iteration uses a traditionalforloop with an index variable. - TypeScript uses
number[]syntax. Thefor...ofloop iterates directly over the array, and.lengthreplaces.len().
Objects
An object is a collection of named fields. You create one with curly braces and key: value pairs:
var person = { name: "Alice", age: 30 }The type is inferred from the fields — person has fields name (type str) and age (type int). You access fields with dot notation (person.name) and assign new values with dot notation (person.age = 31). Because we declared person with var, the object is mutable.
This prints:
Alice
30
31Each language handles object-like data differently:
- Rust uses
AutoObject, a dynamic key-value store. Fields are set and retrieved with.set()and.get(), and values are wrapped inAutoValue. - Python maps directly to a dictionary
dict. Dot access in Auto becomes bracket accessperson["name"]in Python. - C generates a
typedef structwith named fields. This is the closest mapping — C structs and Auto objects both use fixed layouts with dot access. - TypeScript uses an inline type
{ name: string; age: number }. Dot access works just like Auto.
HashMaps
When an object literal uses string keys, Auto infers a HashMap instead of a plain object:
var scores = { "Alice": 90, "Bob": 85, "Charlie": 92 }The difference from a plain object is that you access values with bracket notation (scores["Alice"]) and the map can grow — you can insert new keys at runtime. The type is inferred as Map<str, int>.
This prints:
90
4The key difference between objects and HashMaps in Auto is the key type. Objects use identifier keys ({ name: "Alice" }) and are accessed with dot notation. HashMaps use string keys ({ "Alice": 90 }) and are accessed with bracket notation. The compiler figures out which one you mean from the syntax.
Look at how each language handles this:
- Rust uses
std::collections::HashMap<String, i64>with.insert()and.get(). The.unwrap()on.get()is safe here because we know the key exists. - Python uses a plain
dict— the same syntax as Auto. Bracket access andlen()work naturally. - C has no built-in hash map, so the transpiler generates a linked-list-based map with
map_insertandmap_gethelper functions. - TypeScript uses an index signature
{ [key: string]: number }andObject.keys()for the length.
Sets
A set is a collection of unique values. You create one with curly braces and comma-separated values — the same syntax as an object, but with non-string elements:
var unique = {1, 2, 3, 2, 1}Duplicate values are automatically discarded. The compiler infers a Set<int> because the elements are integers. You can add elements with .add() and check membership with .contains().
This prints:
3
trueThe set starts with five values but only three are unique, so len() returns 3. After adding 4, contains(3) returns true because 3 was in the original set.
Notice the language differences:
- Rust uses
HashSet<i64>fromstd::collections. The.insert()method silently ignores duplicates, and.contains()takes a reference. - Python uses a native
setliteral. Theinoperator replaces Auto's.contains()method. - C generates a linked-list-based set with helper functions. Duplicates are checked before insertion.
- TypeScript uses
Set<number>. The.sizeproperty replaces.len(), and.has()replaces.contains().
How does Auto tell the difference between a set {1, 2, 3}, an object { name: "Alice" }, and a HashMap { "Alice": 90 }? By the keys:
- Identifier keys (no quotes):
{ name: "Alice" }is an object. - String keys (quoted):
{ "Alice": 90 }is a HashMap. - No keys (just values):
{1, 2, 3}is a Set.
The compiler uses these syntactic cues to infer the correct type. You never need to write HashMap<String, i64> or HashSet<int> explicitly — Auto figures it out.
List Operations
Arrays support a rich set of operations beyond simple indexing. You can add elements with .push(), remove the last element with .pop(), check membership with .contains(), and sort in place with .sort().
This prints:
[10, 20, 30, 40]
40
true
[10, 20, 30]The list operations map to familiar patterns in each language:
- Rust uses
Vec::push(),Vec::pop()(returnsOption<i64>), andVec::contains(). Debug formatting with{:?}prints the whole vector. - Python uses
.append()instead of.push(), and theinoperator instead of.contains()..sort()works the same way. - C needs a custom
Vecstruct with manual memory management.qsortfrom the standard library handles sorting. - TypeScript uses
.push(),.pop(), and.includes(). The.sort()method needs a comparator function(a, b) => a - bto sort numerically — without it, TypeScript sorts lexicographically.
Strings as Collections
Strings in Auto are collections of characters. You can get the length with .len(), access individual characters by index, split a string into parts with .split(), and convert case with .to_upper() and .to_lower().
This prints:
12
H
["Hello", "Auto!"]
HELLO, AUTO!String methods are consistent across all four target languages, but the details vary:
- Rust uses
.len()for byte length,.chars().nth(0)for character access (because Rust strings are UTF-8),.split(", ").collect()for splitting, and.to_uppercase()for case conversion. - Python uses
len(s)as a built-in function,s[0]for indexing,s.split(", ")for splitting, ands.upper()for uppercasing. - C uses
strlen()for length, direct array indexings[0],strtok()for splitting, and a manual loop withtoupper()for case conversion. - TypeScript uses
.length(a property, not a method),s[0]for indexing,.split(", ")for splitting, and.toUpperCase()for case conversion.
Nodes
Arrays, objects, and maps are all contiguous data structures — their elements are stored next to each other in memory. Sometimes you need a linked structure, where each element points to the next one. Think of a chain: each link connects to the next link, and the last link connects to nothing.
Auto provides the node keyword for this pattern. A node is like a lightweight struct where one or more fields can be optional — they might point to another node, or they might be none.
Defining a Node
You define a node with the node keyword, field names, and types. The ? prefix on a type means "optional" — the value can either be a real instance or none:
node Item {
value int
next ?Item
}This defines a node called Item with two fields: value (an integer) and next (either another Item or none). The ?Item type is Auto's way of saying "nullable Item" — similar to Option<Item> in Rust or Item | null in TypeScript.
This prints:
10
20We build the chain from back to front. First third (value 30, next is none), then second (value 20, next points to third), then first (value 10, next points to second). Accessing first.next.value follows the link from the first node to the second and reads its value.
The transpiler maps node to each language's idiomatic linked structure:
- Rust uses a
structwithOption<Rc<RefCell<Item>>>for thenextfield.Rc(reference counting) allows multiple ownership, andRefCellprovides interior mutability. This is the standard Rust pattern for linked structures. - Python uses a
classwith__init__. Thenextfield defaults toNone. Python's dynamic typing makes this natural. - C uses a
typedef structwith a pointerstruct Item* next. Theitem_new()helper function allocates memory withmalloc().NULLreplacesnone. - TypeScript uses a
classwithnext: Item | null. The non-null assertionfirst.next!.valuetells TypeScript the value is not null at that point.
Traversing a Node Chain
Accessing first.next.value works for two levels, but real linked structures can be arbitrarily long. The standard pattern is to start at the head and walk through each node until you reach none:
This prints:
10
20
30The traversal pattern is the same in every language:
- Start with
current = first(the head of the chain). - Loop while
currentis notnone/NULL/null. - Print
current.value. - Move to the next node:
current = current.next.
In Auto, for current != none { ... } is a conditional loop — it keeps running as long as the condition is true. The transpiler converts this to while in every target language, since for cond is Auto's way of writing what other languages call while.
Notice that the traversal code is nearly identical across all five languages. Linked list traversal is one of those patterns where the logic is universal — only the syntax for "is this null?" changes:
| Language | Null check | Null value |
|---|---|---|
| Auto | current != none | none |
| Rust | current.is_some() | None |
| Python | current is not None | None |
| C | current != NULL | NULL |
| TypeScript | current !== null | null |
Practical Sorting and Matrix Operations
The collections above demonstrate the building blocks. Here are three common operations you will use frequently: sorting primitives, sorting structs by key, and matrix arithmetic.
Summary
In this chapter, you learned how to work with collections and linked structures in Auto:
- Arrays
[1, 2, 3]store ordered sequences. Access by index witharr[0], get length witharr.len(), iterate withfor item in arr. - Objects
{ name: "Alice" }store named fields. Access with dot notation (obj.name), mutate with assignment (obj.age = 31). - HashMaps
{ "Alice": 90 }are inferred from string-keyed object literals. Access with brackets (map["Alice"]), insert with bracket assignment. - Sets
{1, 2, 3}store unique values. Add with.add(), check membership with.contains(). - List operations —
.push(),.pop(),.contains(),.sort()— let you manipulate arrays in place. - Strings as collections —
.len(),s[0],.split(),.to_upper()— treat strings as sequences of characters. - Nodes with
nodeand?Tlet you build linked structures.nonemarks the end of a chain. Traversal usesfor current != none { ... }.
The compiler infers collection types from syntax — you never need to write Vec<i64>, HashMap<String, i64>, or HashSet<i64>. Auto reads your intent from the literal and picks the right type. In the next chapter, we'll look at how to define your own custom types with type and build methods with ext.