Skip to content
📖 The Auto Programming LanguageBook contents

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, and none

Arrays ​

An array is an ordered, indexed collection of values. You create one with square brackets and a comma-separated list of elements:

auto
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.

Creating arrays, indexing, and iterating

This prints:

1
5
Alice
Bob
Charlie

Notice how the transpiler adapts the array concept to each language:

  • Rust uses Vec<i64> — a heap-allocated dynamic vector. The for name in &names loop borrows each element.
  • Python keeps the native list syntax [1, 2, 3]. The len() built-in replaces Auto's .len() method.
  • C uses a plain C array long long numbers[] with a separate int for the length. Iteration uses a traditional for loop with an index variable.
  • TypeScript uses number[] syntax. The for...of loop iterates directly over the array, and .length replaces .len().

Objects ​

An object is a collection of named fields. You create one with curly braces and key: value pairs:

auto
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.

Creating an object, reading and writing fields

This prints:

Alice
30
31

Each 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 in AutoValue.
  • Python maps directly to a dictionary dict. Dot access in Auto becomes bracket access person["name"] in Python.
  • C generates a typedef struct with 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:

auto
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>.

HashMap inferred from string-keyed literal

This prints:

90
4

The 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 and len() work naturally.
  • C has no built-in hash map, so the transpiler generates a linked-list-based map with map_insert and map_get helper functions.
  • TypeScript uses an index signature { [key: string]: number } and Object.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:

auto
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().

Set literal with deduplication and membership test

This prints:

3
true

The 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> from std::collections. The .insert() method silently ignores duplicates, and .contains() takes a reference.
  • Python uses a native set literal. The in operator 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 .size property 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().

Push, pop, contains, and sort on a list

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() (returns Option<i64>), and Vec::contains(). Debug formatting with {:?} prints the whole vector.
  • Python uses .append() instead of .push(), and the in operator instead of .contains(). .sort() works the same way.
  • C needs a custom Vec struct with manual memory management. qsort from the standard library handles sorting.
  • TypeScript uses .push(), .pop(), and .includes(). The .sort() method needs a comparator function (a, b) => a - b to 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().

String length, indexing, split, and case conversion

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, and s.upper() for uppercasing.
  • C uses strlen() for length, direct array indexing s[0], strtok() for splitting, and a manual loop with toupper() 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:

auto
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.

Defining a node type and building a linked chain

This prints:

10
20

We 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 struct with Option<Rc<RefCell<Item>>> for the next field. Rc (reference counting) allows multiple ownership, and RefCell provides interior mutability. This is the standard Rust pattern for linked structures.
  • Python uses a class with __init__. The next field defaults to None. Python's dynamic typing makes this natural.
  • C uses a typedef struct with a pointer struct Item* next. The item_new() helper function allocates memory with malloc(). NULL replaces none.
  • TypeScript uses a class with next: Item | null. The non-null assertion first.next!.value tells 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:

Traversing a linked chain with a conditional loop

This prints:

10
20
30

The traversal pattern is the same in every language:

  1. Start with current = first (the head of the chain).
  2. Loop while current is not none / NULL / null.
  3. Print current.value.
  4. 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:

LanguageNull checkNull value
Autocurrent != nonenone
Rustcurrent.is_some()None
Pythoncurrent is not NoneNone
Ccurrent != NULLNULL
TypeScriptcurrent !== nullnull

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.

Sorting integers in ascending order
Sorting structs by a field using sort_by_key
2D matrix addition with nested arrays

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 with arr[0], get length with arr.len(), iterate with for 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 node and ?T let you build linked structures. none marks the end of a chain. Traversal uses for 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.

Released under the MIT License.