Types & let
Over the first five chapters, you've been writing Auto as a scripting language — declaring variables with var, relying on type inference, and using anonymous objects to group data. That approach got you started quickly, and it's one of Auto's strengths. But there's a deeper layer to the language.
In this chapter — the first chapter of Phase 2 — we're going to grow up. You'll learn why immutability matters, how to attach explicit types to your variables, how to define custom data types with type, how to attach methods with ext, and how to use tuples and type aliases. These are the tools that let you write programs that are not just correct, but clear, safe, and maintainable.
In this chapter, we'll cover:
let— immutable bindings and why they matter- Type annotations — explicit types on variables
- Custom types — defining structured data with
type - Constructors and field access — creating instances and reading fields
- Methods with
ext— attaching behavior to types,&self - Tuples — fixed-size heterogeneous collections
- Type aliases — naming types for clarity
let — Immutable by Default
In Phase 1, every variable was declared with var. The var keyword creates a mutable binding — you can reassign it as many times as you like. That's fine for counters and accumulators, but most of the time you don't actually need to change a value after you create it.
Auto provides let for immutable bindings. Once you assign a value to a let binding, you cannot reassign it. This is not a limitation — it's a safety feature. When you read let name = "Alice", you know with certainty that name will always refer to "Alice" for the rest of its scope. No function you call can silently change it.
This prints:
6
10Notice the commented-out line // y = 11. If you uncomment it, the Auto compiler will refuse to compile your program. This is a compile-time error, not a runtime one. The compiler catches the mistake before your program ever runs.
Look at how each language expresses immutability:
- Rust uses
letfor immutable bindings andlet mutfor mutable ones. Auto'sletmaps to Rust'slet, and Auto'svarmaps to Rust'slet mut. - Python has no built-in immutability enforcement for variables. The transpiler converts
letto a regular assignment with a comment noting the semantic difference. - C uses
constfor immutable variables. Auto'sletmaps toconst, and Auto'svarmaps to a plain declaration. - TypeScript uses
constfor immutable bindings andletfor mutable ones. This is the opposite of Rust's naming, but the semantics are the same.
Here's the mapping side by side:
| Auto | Rust | Python | C | TypeScript |
|---|---|---|---|---|
var x = 5 | let mut x = 5 | x = 5 | long long x = 5 | let x = 5 |
let y = 10 | let y = 10 | y = 10 | const long long y = 10 | const y = 10 |
Going forward, use let whenever a value doesn't need to change. Reserve var for cases where reassignment is truly necessary. This isn't just a style preference — it makes your code easier to reason about, and it helps the compiler generate better code.
Type Annotations
Auto can infer most types for you. When you write var x = 5, the compiler knows x is an int. When you write let name = "Alice", it knows name is a String. But sometimes you want to be explicit — to document your intent, to catch errors early, or to satisfy a requirement of a target language.
Auto lets you add a type annotation after the variable name:
var count int = 0
let name String = "Alice"The syntax is name Type — the variable name, then the type, separated by a space. This is the same space-separated syntax used in type definitions and function parameters.
This prints:
0
Alice
3.14159
trueAuto's basic types map to each target language as follows:
| Auto | Rust | Python | C | TypeScript |
|---|---|---|---|---|
int | i64 | int | long long | number |
float | f64 | float | double | number |
String | String | str | const char* | string |
bool | bool | bool | bool | boolean |
A few things to notice:
- Auto uses
String(capitalized) for heap-allocated text strings, matching Rust's convention. This is distinct fromstr, which in Rust refers to a borrowed string slice. - Both
intandfloatmap tonumberin TypeScript, since JavaScript has no distinction between integer and floating-point types at the language level. - C strings are
const char*— a pointer to null-terminated characters. The C transpiler usesprintfwith%sfor string output.
When should you annotate types? A good rule of thumb: annotate when the type isn't obvious from the value. var count int = 0 is helpful because 0 alone could be int or float. But let name = "Alice" is clear enough — the type is obviously a string. Auto never requires annotations, but it always accepts them.
Custom Types
In Phase 1, you grouped related data with anonymous objects: { name: "Alice", age: 30 }. These work well for quick scripts, but they have limitations. You can't reuse the shape — every { name, age } object is independent. You can't attach methods to them. And the compiler can't help you catch field name typos at compile time.
The type keyword solves all of these problems. It lets you define a named type — a blueprint that describes what fields a value has and what types those fields are:
type User {
name String
age int
active bool
}This defines a type called User with three fields: name (a String), age (an int), and active (a bool). Note the syntax: each field is name Type, separated by spaces. No commas, no colons — just the field name and its type.
This prints:
Alice
30Each target language maps type to its closest equivalent:
- Rust uses
struct. The field syntax changes fromname Type(Auto) toname: Type(Rust). String literals needString::from()wrapping. - Python uses a
@dataclassclass. Fields become constructor parameters with keyword argument syntax. - C uses a
typedef structwith fixed-size character arrays for strings. Field assignment is done individually withstrcpyfor strings. - TypeScript uses a
classwith a constructor. The Auto named-field constructor syntax maps to constructor arguments.
The named-field constructor syntax User { name: "Alice", age: 30 } is common across Auto, Rust, and TypeScript. Python uses keyword arguments instead (User(name="Alice", age=30)), and C uses individual field assignments after declaration.
Constructors and Field Access
Every type definition automatically generates a constructor — a way to create instances. You've already seen this in action: User { name: "Alice", age: 30, active: true }. The constructor takes the type name followed by curly braces with field: value pairs.
Once you have an instance, you access its fields with dot notation: alice.name, alice.age. This works the same way in all four target languages.
This prints:
50The area function takes a Rectangle parameter and returns the product of its width and height. Notice a few important details:
- The return type of
areais inferred asfloatfrom the expression `r.width- r.height
. You don't need to writefn area(r Rectangle) float { ... }`, though you could.
- r.height
- The
rparameter is passed by value — the function receives a copy of the rectangle. This is the default in Auto (and in Rust and C). The originalrectinmainis unchanged. - The C transpiler passes structs by value too. The
areafunction receives a copy of the entire struct on the stack.
Compare this to Phase 1's anonymous objects. With { name: "Alice", age: 30 }, you got a dynamic bag of fields. With type User { name String, age int }, you get a type — a named shape that the compiler understands, checks, and can use to catch errors.
Methods with ext
Data without behavior is just a record. Real programs need to attach operations to their data — calculating the distance of a point, translating a coordinate, formatting a user's full name. Auto provides the ext keyword for this purpose. It lets you add methods to a type without modifying the type definition itself.
This prints:
5
4The point (3.0, 4.0) has a distance of 5.0 from the origin (the classic 3-4-5 right triangle). Translating by (1.0, 2.0) produces a new point (4.0, 6.0), and we print its x-coordinate: 4.
Let's break down the ext block:
ext Point { ... }attaches methods to thePointtype. The methods are defined inside the block, just like functions, but they have access toself.&selfis the first parameter of each method. It refers to the instance the method is called on. When you writep.distance(), theselfinside the method body refers top.distance(&self) floattakes no additional parameters and returns afloat. It computes the Euclidean distance from the origin using the Pythagorean theorem.translate(&self, dx float, dy float) Pointtakes two additional parameters and returns a newPoint. The original point is unchanged — this is the immutable-by-default philosophy in action.
The &self parameter has a special meaning across all target languages:
| Language | Mechanism | Auto's &self maps to |
|---|---|---|
| Rust | impl block with &self | &self (identical) |
| Python | Instance method with self | self (first parameter) |
| C | Function with pointer parameter | Point* self (pointer) |
| TypeScript | Class method using this | this (implicit reference) |
In Rust, ext maps directly to impl — the standard way to define methods on a struct. In Python, methods become part of the @dataclass body. In C, methods become free functions that take a pointer to the struct. In TypeScript, methods are defined directly inside the class.
The & prefix in &self is borrowed from Rust. It signals that the method takes a reference to the instance rather than consuming it. This is a hint about the underlying memory model: the method can read the data but doesn't own it. For now, the important thing to remember is that &self means "this method operates on an instance of the type."
Tuples
Sometimes you need to group a few values of different types without defining a full type. A tuple is a fixed-size, ordered collection where each position can have a different type. You create one with parentheses and comma-separated values:
var pair = (1, "hello")This tuple has two elements: an int at position 0 and a String at position
- The type is inferred as
(int, String).
This prints:
1
hello
1
helloThere are two ways to access tuple elements:
- Positional access:
pair.0andpair.1access the first and second elements by their zero-based position. This works in Auto and Rust. - Destructuring:
var (a, b) = pairunpacks the tuple into two separate variables. This is a concise way to extract all elements at once.
Each language handles tuples differently:
- Rust has native tuple support. Positional access with
.0,.1works directly. Destructuring withlet (a, b) = pairis also native. - Python uses indexing with
pair[0],pair[1]. Destructuring witha, b = pairis a Python feature called tuple unpacking. - C has no tuples, so the transpiler generates a
typedef structwith fields named_0,_1. Destructuring becomes individual field assignments. - TypeScript maps tuples to arrays with a fixed type signature
[number, string]. Both indexing and destructuring work naturally.
Tuples are useful when you need to return multiple values from a function or group a small, temporary collection of related values. They're lightweight — no type definition required. But if you find yourself using the same tuple shape repeatedly, consider defining a type instead. A named type is always clearer than an unnamed one.
Type Aliases
Sometimes a type name doesn't convey enough meaning on its own. int could be a count, an ID, an index, or an age. float could be a coordinate, a weight, or a temperature. A type alias gives a meaningful name to an existing type without creating a new one:
type Point = (float, float)
type Id = intThe type keyword serves double duty: with curly braces it defines a custom type, and with = it creates an alias. The alias is just another name for the same underlying type — Id is exactly the same as int, but it tells the reader (and the compiler) that this value is meant to be used as an identifier.
This prints:
(0.0, 0.0)
42Type aliases are especially useful in larger programs. When you see a function signature like fn lookup(id Id) User, you immediately know that id is a user identifier, not just some arbitrary integer. Compare that to fn lookup(id int) User — the type is correct, but the meaning is lost.
Each target language has its own mechanism for type aliases:
- Rust uses the
typekeyword (identical to Auto's syntax):type Point = (f64, f64);. - Python uses simple assignment:
Point = tuple[float, float]. This is a runtime alias — Python doesn't distinguish betweenPointand the underlying tuple type. - C uses
typedeffor both aliases and struct definitions.typedef long long Id;creates an alias, andtypedef struct { ... } Point;creates a struct type. - TypeScript uses the
typekeyword (identical to Auto):type Point = [number, number];.
Summary
This chapter marks the transition from Phase 1 to Phase 2. You've graduated from using Auto as a simple scripting language to understanding its type system. Here's what you learned:
letcreates immutable bindings. Useletby default andvaronly when you need to reassign. The compiler enforces immutability at compile time.- Type annotations —
var count int = 0— let you explicitly state a variable's type. They're optional but useful for documentation and catching errors. - Custom types with
typereplace Phase 1's anonymous objects. Atypedefinition creates a named, reusable shape with named fields. - Constructors and field access —
User { name: "Alice" }creates an instance, andalice.namereads a field. Instances are passed by value by default. - Methods with
extlet you attach functions to types. The&selfparameter refers to the instance the method is called on. - Tuples
(1, "hello")group a fixed number of values with different types. Access elements by position (.0,.1) or destructure withvar (a, b) = pair. - Type aliases —
type Id = int— give meaningful names to existing types for clarity.
The type keyword is the most versatile tool in this chapter. With curly braces, it defines a struct-like type. With =, it creates an alias. And with ext, you can add methods to any type you've defined. Together, these features form the foundation of Auto's type system — a system that's expressive enough for complex programs but simple enough that you don't need to write boilerplate.
In the next chapter, we'll explore pattern matching — a powerful way to inspect and destructure values based on their shape.