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:
- Starts with
def - Has
selfas its first parameter - Can have additional parameters after
self - 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():
- The object
cbecomesself - The method body executes with that
self self.countaccessesc’scountfield
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():
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(())
}
For object.method(args):
- Type check object - Get the object’s type
- Verify it is a class - Cannot call methods on
intorbool - Find the method - Look up method name in the class
- Check arguments - Verify argument types match parameters
- 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
| Concept | Syntax | Description |
|---|---|---|
| Method definition | def name(self, ...) | Function belonging to a class |
| Method call | obj.method(args) | Call method on an object |
| Self reference | self | The object being operated on |
| Field read | self.field | Access object’s field |
| Field write | self.field = x | Modify 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.