Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Methods and Self

If you have not read about functions in Firstlang, review that first. Methods are similar to functions but attached to objects.

Methods are functions that belong to a class. They always receive the object as their first parameter, called self.

Method Definition

Methods are defined inside a class using def:

class Counter {
    count: int

    def __init__(self) {
        self.count = 0
    }

    def increment(self) -> int {
        self.count = self.count + 1
        return self.count
    }

    def add(self, n: int) -> int {
        self.count = self.count + n
        return self.count
    }

    def get(self) -> int {
        return self.count
    }
}

Every method:

  1. Starts with def
  2. Has self as its first parameter
  3. Can have additional parameters after self
  4. Can have a return type

The self Parameter

self is a reference to the object the method was called on:

c = new Counter()
c.increment()   # Inside increment(), self == c

When you call c.increment():

  1. The object c becomes self
  2. The method body executes with that self
  3. self.count accesses c’s count field

Self is Explicit

Unlike Java or C++ (where this is implicit), Thirdlang requires explicit self:

# Thirdlang (explicit self)
def get(self) -> int {
    return self.count    # Must write self.
}

# Java (implicit this)
// int get() {
//     return count;     // 'this.' is optional
// }

Explicit self is clearer: you always know when you are accessing object state.

Method Calls

Call a method using dot notation:

c = new Counter()
c.increment()       # Call increment on c
c.add(5)            # Call add with argument 5
x = c.get()         # Get returns an int

How Method Calls Work

When you write c.increment():


How method works

Methods are compiled as regular functions with a mangled name: ClassName__methodName.

Type Checking Methods

The type checker verifies method calls:

fn typecheck_expr(
    ctx: &mut TypeContext,
    expr: &mut TypedExpr,
    env: &TypeEnv,
) -> Result<(), String> {
    match &mut expr.expr {
        Expr::Int(_) => {
            expr.ty = Type::Int;
        }

        Expr::Bool(_) => {
            expr.ty = Type::Bool;
        }

        Expr::Var(name) => {
            if let Some(ty) = env.get(name) {
                expr.ty = ty.clone();
            } else {
                return Err(format!("Undefined variable: {}", name));
            }
        }

        Expr::SelfRef => {
            if let Some(class_name) = &ctx.current_class {
                expr.ty = Type::Class(class_name.clone());
            } else {
                return Err("'self' can only be used inside a method".to_string());
            }
        }

        Expr::Unary { op, expr: inner } => {
            typecheck_expr(ctx, inner, env)?;
            match op {
                UnaryOp::Neg => {
                    if inner.ty != Type::Int {
                        return Err(format!("Cannot negate non-integer type: {}", inner.ty));
                    }
                    expr.ty = Type::Int;
                }
                UnaryOp::Not => {
                    if inner.ty != Type::Bool {
                        return Err(format!("Cannot negate non-boolean type: {}", inner.ty));
                    }
                    expr.ty = Type::Bool;
                }
            }
        }

        Expr::Binary { op, left, right } => {
            typecheck_expr(ctx, left, env)?;
            typecheck_expr(ctx, right, env)?;

            match op {
                BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
                    if left.ty != Type::Int || right.ty != Type::Int {
                        return Err(format!(
                            "Arithmetic operation requires int operands, got {} and {}",
                            left.ty, right.ty
                        ));
                    }
                    expr.ty = Type::Int;
                }
                BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
                    if left.ty != Type::Int || right.ty != Type::Int {
                        return Err(format!(
                            "Comparison requires int operands, got {} and {}",
                            left.ty, right.ty
                        ));
                    }
                    expr.ty = Type::Bool;
                }
                BinaryOp::Eq | BinaryOp::Ne => {
                    let _ = left.ty.unify(&right.ty)?;
                    expr.ty = Type::Bool;
                }
            }
        }

        Expr::Call { name, args } => {
            // Look up function type
            let func_type = env
                .get(name)
                .or_else(|| ctx.global_env.get(name))
                .ok_or_else(|| format!("Undefined function: {}", name))?
                .clone();

            if let Type::Function { params, ret } = func_type {
                if args.len() != params.len() {
                    return Err(format!(
                        "Function {} expects {} arguments, got {}",
                        name,
                        params.len(),
                        args.len()
                    ));
                }

                for (arg, param_type) in args.iter_mut().zip(params.iter()) {
                    typecheck_expr(ctx, arg, env)?;
                    let _ = arg.ty.unify(param_type)?;
                }

                expr.ty = *ret;
            } else {
                return Err(format!("{} is not a function", name));
            }
        }

        Expr::MethodCall {
            object,
            method,
            args,
        } => {
            typecheck_expr(ctx, object, env)?;

            // Get the class info
            let class_name = object
                .ty
                .class_name()
                .ok_or_else(|| format!("Cannot call method on non-class type: {}", object.ty))?;

            let class_info = ctx
                .classes
                .get(class_name)
                .ok_or_else(|| format!("Unknown class: {}", class_name))?
                .clone();

            let method_info = class_info
                .get_method(method)
                .ok_or_else(|| format!("Unknown method {} on class {}", method, class_name))?
                .clone();

            // Check argument count
            if args.len() != method_info.params.len() {
                return Err(format!(
                    "Method {}.{} expects {} arguments, got {}",
                    class_name,
                    method,
                    method_info.params.len(),
                    args.len()
                ));
            }

            // Type check arguments
            for (arg, (_, param_type)) in args.iter_mut().zip(method_info.params.iter()) {
                typecheck_expr(ctx, arg, env)?;
                let _ = arg.ty.unify(param_type)?;
            }

            expr.ty = method_info.return_type.clone();
        }

        Expr::FieldAccess { object, field } => {
            typecheck_expr(ctx, object, env)?;

            // Get the class info
            let class_name = object
                .ty
                .class_name()
                .ok_or_else(|| format!("Cannot access field on non-class type: {}", object.ty))?;

            let class_info = ctx
                .classes
                .get(class_name)
                .ok_or_else(|| format!("Unknown class: {}", class_name))?;

            let field_type = class_info
                .get_field(field)
                .ok_or_else(|| format!("Unknown field {} on class {}", field, class_name))?;

            expr.ty = field_type.clone();
        }

        Expr::New { class, args } => {
            // Get the class info
            let class_info = ctx
                .classes
                .get(class)
                .ok_or_else(|| format!("Unknown class: {}", class))?
                .clone();

            // Get constructor if exists
            if let Some(ctor) = class_info.get_method("__init__") {
                if args.len() != ctor.params.len() {
                    return Err(format!(
                        "Constructor for {} expects {} arguments, got {}",
                        class,
                        ctor.params.len(),
                        args.len()
                    ));
                }

                for (arg, (_, param_type)) in args.iter_mut().zip(ctor.params.iter()) {
                    typecheck_expr(ctx, arg, env)?;
                    let _ = arg.ty.unify(param_type)?;
                }
            } else if !args.is_empty() {
                return Err(format!(
                    "Class {} has no constructor but {} arguments provided",
                    class,
                    args.len()
                ));
            }

            expr.ty = Type::Class(class.clone());
        }

        Expr::If {
            cond,
            then_branch,
            else_branch,
        } => {
            typecheck_expr(ctx, cond, env)?;
            if cond.ty != Type::Bool {
                return Err(format!("If condition must be bool, got {}", cond.ty));
            }

            let mut then_env = env.clone();
            let mut then_type = Type::Unit;
            for stmt in then_branch.iter_mut() {
                then_type = typecheck_stmt(ctx, stmt, &mut then_env)?;
            }

            let mut else_env = env.clone();
            let mut else_type = Type::Unit;
            for stmt in else_branch.iter_mut() {
                else_type = typecheck_stmt(ctx, stmt, &mut else_env)?;
            }

            expr.ty = then_type.unify(&else_type)?;
        }

        Expr::While { cond, body } => {
            typecheck_expr(ctx, cond, env)?;
            if cond.ty != Type::Bool {
                return Err(format!("While condition must be bool, got {}", cond.ty));
            }

            let mut body_env = env.clone();
            for stmt in body.iter_mut() {
                typecheck_stmt(ctx, stmt, &mut body_env)?;
            }

            expr.ty = Type::Unit;
        }

        Expr::Block(stmts) => {
            let mut block_env = env.clone();
            let mut last_type = Type::Unit;
            for stmt in stmts.iter_mut() {
                last_type = typecheck_stmt(ctx, stmt, &mut block_env)?;
            }
            expr.ty = last_type;
        }
    }

    Ok(())
}

thirdlang/src/typeck.rs

For object.method(args):

  1. Type check object - Get the object’s type
  2. Verify it is a class - Cannot call methods on int or bool
  3. Find the method - Look up method name in the class
  4. Check arguments - Verify argument types match parameters
  5. Return method’s return type - The expression’s type

Example Type Checks

class Point {
    x: int
    y: int
    def __init__(self, x: int, y: int) { ... }
    def distance_squared(self, other: Point) -> int { ... }
}

p1 = new Point(0, 0)
p2 = new Point(3, 4)

p1.distance_squared(p2)      # OK: Point method, Point argument
p1.distance_squared(5)       # ERROR: expected Point, got int
p1.nonexistent()             # ERROR: method not found

Methods with Parameters

Methods can take additional parameters after self:

class Point {
    x: int
    y: int

    def __init__(self, x: int, y: int) {
        self.x = x
        self.y = y
    }

    def distance_squared(self, other: Point) -> int {
        dx = other.x - self.x
        dy = other.y - self.y
        return dx * dx + dy * dy
    }

    def translate(self, dx: int, dy: int) {
        self.x = self.x + dx
        self.y = self.y + dy
    }
}

The distance_squared method takes another Point and returns int. The translate method takes two integers and has no explicit return type - this means it returns Unit (nothing useful), it just modifies the object in place.

Classes as Parameters

Notice other: Point - methods can take objects of any class as parameters:

def distance_squared(self, other: Point) -> int { ... }
#                          ^^^^^^^^^^^^
#                          Parameter is a Point!

Important: When you pass an object as a parameter, you are passing a pointer (reference), not a copy. Both self and other point to actual objects in memory. If you modify other.x inside the method, you are modifying the original object!

This is called reference semantics and is common in OOP languages (Java, Python, etc.).

Field Access

Methods can read and write fields using self.field:

def get_x(self) -> int {
    return self.x        # Read field
}

def set_x(self, x: int) {
    self.x = x           # Write field
}

Field Access Compiles to Struct GEP

In LLVM IR, self.x becomes a GEP (Get Element Pointer) instruction. GEP is one of LLVM’s most important instructions - it calculates the memory address of a struct field without actually reading memory. Think of it as “pointer arithmetic” that knows about struct layouts:

; self.x = 42
%field_ptr = getelementptr %Point, ptr %self, i32 0, i32 0  ; Get pointer to field 0
store i64 42, ptr %field_ptr                                 ; Store value

; return self.x
%field_ptr = getelementptr %Point, ptr %self, i32 0, i32 0
%value = load i64, ptr %field_ptr
ret i64 %value

Method Compilation

Methods compile to regular functions with a special naming convention:

class Point {
    def get_x(self) -> int {
        return self.x
    }
}

Becomes:

define i64 @Point__get_x(ptr %self) {
    %x_ptr = getelementptr %Point, ptr %self, i32 0, i32 0
    %x = load i64, ptr %x_ptr
    ret i64 %x
}

The naming pattern ClassName__methodName:

  • Avoids conflicts between classes
  • Makes it clear which class owns the method
  • Allows us to find the right function at call sites

Calling Methods on Self

Inside a method, you can call other methods on self:

class Calculator {
    value: int

    def __init__(self) {
        self.value = 0
    }

    def add(self, n: int) {
        self.value = self.value + n
    }

    def double(self) {
        self.add(self.value)   # Call add on self
    }
}

This is just self.methodName(args) - same as any other method call.

Methods Returning Self’s Type

Methods can return their own class type:

class Builder {
    value: int

    def __init__(self) { self.value = 0 }

    def set_value(self, v: int) -> Builder {
        self.value = v
        return self   # Return the same object
    }
}

b = new Builder()
b.set_value(10)

Returning self enables method chaining (though we do not support it syntactically yet).

Special Methods

Constructor: __init__

Called automatically when you use new:

def __init__(self, x: int) {
    self.x = x
}

Destructor: __del__

Called automatically when you use delete:

def __del__(self) {
    # Cleanup code (if any)
}

We cover destructors in detail in the memory management chapter.

Summary

ConceptSyntaxDescription
Method definitiondef name(self, ...)Function belonging to a class
Method callobj.method(args)Call method on an object
Self referenceselfThe object being operated on
Field readself.fieldAccess object’s field
Field writeself.field = xModify object’s field

Methods are the behavior half of object-oriented programming. Fields are data; methods are actions.

Next, let us look at memory management - how objects are allocated and freed.