Composition
Forge supports struct composition through the has keyword, which embeds one struct inside another. Embedded fields enable automatic delegation of both field access and method calls to the inner struct, providing a composition-based alternative to inheritance.
Syntax
EmbedField = "has" Identifier ":" TypeAnnotation
The has keyword appears in a struct field position and marks the field as embedded.
Defining Embedded Fields
Use has inside a struct definition to embed another type:
thing Address {
street: String,
city: String,
zip: String
}
thing Employee {
name: String,
has addr: Address
}
The addr field is an embedded field of type Address. The has keyword tells the runtime to register this field for delegation.
Construction
Embedded fields are initialized like regular fields during construction:
let emp = Employee {
name: "Alice",
addr: Address {
street: "123 Main St",
city: "Springfield",
zip: "62701"
}
}
Field Delegation
When a field is accessed on a struct instance and the field is not found directly on the object, the runtime checks each embedded sub-object for the field. This enables transparent access to inner fields:
// Direct access (always works)
emp.addr.city // "Springfield"
// Delegated access (through embedding)
emp.city // "Springfield"
emp.street // "123 Main St"
The delegation algorithm for obj.field:
- Check if
objhas a direct field namedfield. If found, return it. - Read
obj.__type__to get the type name. - Look up the type name in
embedded_fieldsto get the list of(field_name, type_name)pairs. - For each embedded field, check if
obj[field_name]is an object with the requestedfield. If found, return it. - If no embedded field contains the requested field, raise a runtime error.
Method Delegation
Method calls are also delegated to embedded types. If a method is not found in the outer type’s method table, the runtime searches each embedded type’s method table:
give Address {
define full(it) {
return it.street + ", " + it.city + " " + it.zip
}
}
// Called on the embedded Address through Employee
emp.full() // "123 Main St, Springfield 62701"
// Explicit path also works
emp.addr.full() // "123 Main St, Springfield 62701"
The method delegation algorithm for obj.method(args):
- Look up
methodinmethod_tables[obj.__type__]. If found, call it withobjprepended asit. - Look up
embedded_fields[obj.__type__]to get the list of embedded fields. - For each
(embed_field, embed_type)pair, look upmethodinmethod_tables[embed_type]. - If found, extract
obj[embed_field]as the receiver and call the method with the sub-object asit. - If no embedded type has the method, continue to builtin resolution or raise an error.
Multiple Embeddings
A struct may embed multiple fields:
thing Engine {
horsepower: Int
}
thing Chassis {
material: String
}
thing Car {
make: String,
has engine: Engine,
has chassis: Chassis
}
give Engine {
define rev(it) {
say "Vroom! " + str(it.horsepower) + "hp"
}
}
give Chassis {
define describe(it) {
return it.material + " chassis"
}
}
let c = Car {
make: "Toyota",
engine: Engine { horsepower: 200 },
chassis: Chassis { material: "Steel" }
}
c.rev() // prints "Vroom! 200hp"
c.describe() // "Steel chassis"
c.horsepower // 200
c.material // "Steel"
Embedded fields are searched in declaration order. If two embedded types provide the same field name, the first match wins.
Embedding and Interfaces
Delegated methods count toward interface satisfaction. If an embedded type’s method table contains a method required by an interface, the outer type satisfies that interface through delegation:
power Describable {
fn describe(it) -> String
}
// Car satisfies Describable through its embedded Chassis
satisfies(c, Describable) // true (via chassis.describe)
Storage
The interpreter maintains an embedded_fields table:
embedded_fields: HashMap<String, Vec<(String, String)>>
The key is the outer struct name. The value is a vector of (field_name, type_name) pairs, one for each has field in the struct definition. This table is populated when the StructDef statement is executed.