Skip to content
📖 The Auto Programming LanguageBook contents

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.

`let` for immutable bindings, `var` for mutable ones

This prints:

6
10

Notice 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 let for immutable bindings and let mut for mutable ones. Auto's let maps to Rust's let, and Auto's var maps to Rust's let mut.
  • Python has no built-in immutability enforcement for variables. The transpiler converts let to a regular assignment with a comment noting the semantic difference.
  • C uses const for immutable variables. Auto's let maps to const, and Auto's var maps to a plain declaration.
  • TypeScript uses const for immutable bindings and let for mutable ones. This is the opposite of Rust's naming, but the semantics are the same.

Here's the mapping side by side:

AutoRustPythonCTypeScript
var x = 5let mut x = 5x = 5long long x = 5let x = 5
let y = 10let y = 10y = 10const long long y = 10const 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:

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

Adding type annotations to variables

This prints:

0
Alice
3.14159
true

Auto's basic types map to each target language as follows:

AutoRustPythonCTypeScript
inti64intlong longnumber
floatf64floatdoublenumber
StringStringstrconst char*string
boolboolboolboolboolean

A few things to notice:

  • Auto uses String (capitalized) for heap-allocated text strings, matching Rust's convention. This is distinct from str, which in Rust refers to a borrowed string slice.
  • Both int and float map to number in 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 uses printf with %s for 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:

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

Creating a custom type with `type`

This prints:

Alice
30

Each target language maps type to its closest equivalent:

  • Rust uses struct. The field syntax changes from name Type (Auto) to name: Type (Rust). String literals need String::from() wrapping.
  • Python uses a @dataclass class. Fields become constructor parameters with keyword argument syntax.
  • C uses a typedef struct with fixed-size character arrays for strings. Field assignment is done individually with strcpy for strings.
  • TypeScript uses a class with 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.

Creating instances and accessing fields

This prints:

50

The area function takes a Rectangle parameter and returns the product of its width and height. Notice a few important details:

  • The return type of area is inferred as float from the expression `r.width
    • r.height. You don't need to write fn area(r Rectangle) float { ... }`, though you could.
  • The r parameter is passed by value — the function receives a copy of the rectangle. This is the default in Auto (and in Rust and C). The original rect in main is unchanged.
  • The C transpiler passes structs by value too. The area function 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.

Attaching methods to a type with `ext`

This prints:

5
4

The 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 the Point type. The methods are defined inside the block, just like functions, but they have access to self.
  • &self is the first parameter of each method. It refers to the instance the method is called on. When you write p.distance(), the self inside the method body refers to p.
  • distance(&self) float takes no additional parameters and returns a float. It computes the Euclidean distance from the origin using the Pythagorean theorem.
  • translate(&self, dx float, dy float) Point takes two additional parameters and returns a new Point. 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:

LanguageMechanismAuto's &self maps to
Rustimpl block with &self&self (identical)
PythonInstance method with selfself (first parameter)
CFunction with pointer parameterPoint* self (pointer)
TypeScriptClass method using thisthis (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:

auto
var pair = (1, "hello")

This tuple has two elements: an int at position 0 and a String at position

  1. The type is inferred as (int, String).
Tuples — fixed-size collections of different types

This prints:

1
hello
1
hello

There are two ways to access tuple elements:

  1. Positional access: pair.0 and pair.1 access the first and second elements by their zero-based position. This works in Auto and Rust.
  2. Destructuring: var (a, b) = pair unpacks 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, .1 works directly. Destructuring with let (a, b) = pair is also native.
  • Python uses indexing with pair[0], pair[1]. Destructuring with a, b = pair is a Python feature called tuple unpacking.
  • C has no tuples, so the transpiler generates a typedef struct with 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:

auto
type Point = (float, float)
type Id = int

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

Creating type aliases for clarity

This prints:

(0.0, 0.0)
42

Type 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 type keyword (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 between Point and the underlying tuple type.
  • C uses typedef for both aliases and struct definitions. typedef long long Id; creates an alias, and typedef struct { ... } Point; creates a struct type.
  • TypeScript uses the type keyword (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:

  • let creates immutable bindings. Use let by default and var only 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 type replace Phase 1's anonymous objects. A type definition creates a named, reusable shape with named fields.
  • Constructors and field access — User { name: "Alice" } creates an instance, and alice.name reads a field. Instances are passed by value by default.
  • Methods with ext let you attach functions to types. The &self parameter 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 with var (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.

Released under the MIT License.