Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/spec/08-generics-interfaces-embedding-methods.md

Generics, Interfaces, Embedding, and Methods

This file is part of the Nocter language specification. The specification entry point is README.md.

v0 Scope

Nocter v0 includes generic type parameters, associated functions, inherent impl blocks, receiver methods, contract-only interface declarations, explicit interface conformance declarations, and Self type syntax inside inherent member and interface method contexts.

Nocter v0 does not include traits. Embedding is an adopted future composition feature, but it is not part of the v0 implementation contract. Generalized ... spread, rest capture, variadic capture, and typed literal construction are specified as a separate future direction in Future Literal Definitions and Spread.

Not part of v0:

  • trait declarations
  • embedding declarations such as ...Type and pub ...Type
  • typed literal construction such as Vec [1, 2, 3]
  • generalized ... spread, rest capture, and variadic capture forms
  • generic bounds such as T: Interface
  • interface-bound method lookup
  • interface objects such as dyn Printable
  • interface inheritance, associated types, default methods, blanket impls, specialization, and where clauses
  • code reuse through interfaces

trait is not a reserved keyword in v0. It is lexed as an identifier. A source form that starts a top-level item with trait is diagnosed as removed syntax, but the spelling remains available as an ordinary identifier in positions such as a function name.

interface is a reserved keyword.

Impl Blocks

Adopted: impl associates receiver methods and destructor members with a nominal type. It is not a class declaration and does not introduce inheritance.

Associated functions are declared at top level with a qualified function name. They have no receiver and are called through the type.

pub func WordStats.empty(): WordStats {
    return WordStats {
        bytes: 0,
        lines: 0,
        words: 0,
    }
}
let stats = WordStats.empty()

method inside an impl defines a receiver method. The receiver is explicit and appears before the method name.

impl WordStats {
    pub method &+self.add_byte(byte: u8): void {
        self.bytes += 1
    }

    pub method &+self.add_word(): void {
        self.words += 1
    }
}

Generic impl blocks declare their own type parameters before the target type. Those parameters are in scope for the impl target, method signatures, drop members, and member bodies.

impl<T> Box<T> {
    pub method self.value(): T {
        return self.value
    }
}
stats.add_word()

Self is type-position syntax inside an inherent impl block and inside a qualified associated function declaration such as func WordStats.empty. It is not an ordinary identifier and is not resolved through normal name lookup. In impl WordStats or func WordStats.empty, Self means WordStats.

self is the fixed receiver name in method and drop member declarations. Self remains type-position syntax. The restrictions on Self are specified in Values and Types.

The target of an impl block must be a nominal type declaration, such as a struct or enum. An impl block cannot target a type alias because aliases do not create distinct types.

type Count = i32

impl Count {
    ...
}
// error: Count is a type alias, not a nominal type

impl Interface for Type declares explicit conformance to an interface. It is not an inherent impl block and cannot contain members.

impl Printable for User
impl Printable for User {}

Generic conformance declarations use the same impl generic parameter list:

impl<T> Source<T> for Box<T>

The implementing methods are ordinary public inherent methods on the target type.

impl User {
    pub method &self.print(): i32 {
        return 0
    }
}

Initial receiver forms:

method &self.name(...): Return
method &+self.name(...): Return
method self.name(...): Return

Meaning:

  • &Self is a readonly receiver.
  • &+Self is a readwrite receiver.
  • Self is a consuming receiver. It requires copy or explicit move according to the normal ownership rules.
  • Calling a &Self method borrows the receiver readonly.
  • Calling a &+Self method borrows the receiver readwrite and requires a writable receiver place.
  • A newly created owned temporary may be used as a &+Self receiver for that single method call because it has no existing aliases.
  • Calling a Self method consumes or copies the receiver according to the receiver type.
  • Borrow-like values derived from a temporary receiver cannot escape the current statement.

Call rules:

  • Type.function(args) calls an associated func.
  • value.method(args) calls a method.
  • Associated function and method arguments follow the positional argument rules in Control Flow.
  • Type.method(&value, args) and Type.method(&+value, args) are invalid in v0.
  • value.function(args) is invalid when function is only an associated func.
  • func Type.name and method share the same member namespace for a type. Defining both with the same member name for the same type is an error in v0.
  • Enum variants also occupy the type member namespace. An associated func or method cannot reuse an enum variant member name in v0.
  • If method lookup finds multiple valid inherent candidates, the call is ambiguous and is a compile error.
  • v0 has no qualified method-call escape hatch for ambiguity resolution.
file.write_text("hello")?          // OK: method call
File.write_text(&+file, "hello")?  // error: methods are not UFCS functions

Method Lookup

Adopted: method lookup is deliberately small and deterministic in v0.

For value.method(args), the compiler first determines the static type of value.

If the receiver has a concrete nominal type, the compiler looks only for inherent methods declared in impl Type blocks for that nominal type.

If the receiver is a generic type parameter, v0 has no interface-bound method lookup. A method call through an unconstrained generic receiver is invalid unless a future feature supplies a bound and lookup rule.

Lookup order:

  1. 1. inherent method on a concrete nominal receiver type
  2. no candidate, producing a compile error

The compiler does not search visible interface conformance declarations to resolve value.method(args) in v0. This avoids import-dependent method lookup and keeps calls readable from the receiver type.

Initial implementation order:

  1. 1. impl Type { ... } receiver methods
  2. Self inside impl and func Type.name
  3. associated function declarations such as func Type.function(...)
  4. associated function calls such as Type.function(...)
  5. method declarations
  6. method calls such as value.method(...)

Interface Contracts

An interface is a contract-only nominal declaration. It may be private, pub, or pub(nocter) like other top-level definitions. Every member inside an interface must be explicitly marked pub.

pub interface Printable {
    pub method &self.print(): i32
}

Rules:

  • Interface members are method signatures only.
  • Interface method signatures cannot have bodies.
  • Interface members cannot be private or pub(nocter).
  • Interfaces cannot declare fields, associated functions, drop members, default methods, associated types, or reusable code.
  • Self inside an interface method signature means the eventual conformance target type for structural checking.
  • An interface conformance declaration is written impl Interface for Type.
  • A conformance declaration may omit {} or use an empty {} body.
  • A conformance declaration body cannot contain members.
  • The conformance target must be a nominal struct or enum.
  • Typechecking verifies that the target has a public inherent method with the same name and signature for every interface method.
  • Method parameter names do not participate in conformance. Receiver type, parameter types, and return type do.
  • Conformance is explicit. A type with matching methods does not satisfy an interface unless source contains impl Interface for Type.

Example:

interface Reader {
    pub method &+self.read_byte(): i32!
}

struct File {
    fd: i32
}

impl File {
    pub method &+self.read_byte(): i32! {
        ...
    }
}

impl Reader for File

This model prevents accidental conformance while keeping the contract check structural. It also keeps code reuse out of v0: interface declarations describe requirements only.

Interface And Embedding Separation

Adopted: Nocter separates contract checking from implementation reuse.

An interface describes a public capability. It does not store data, provide method bodies, forward calls, inject members, or reuse code.

Embedding owns another value inside a struct and promotes only that value's public contract through the embedding owner. It is Nocter's planned composition-based reuse feature. It is not inheritance, not a trait, not a mixin, and not implicit interface conformance.

This separation is part of Nocter's core direction:

  • interface answers "what public capability does this type promise?"
  • embedding answers "what contained value does this type own and expose?"

The two features may work together, but neither feature includes the other. A type that embeds a value does not automatically conform to an interface, and an interface does not provide reusable implementation.

Generics

Adopted: generic type parameters use angle brackets.

struct Buffer<T> {
    ...
}

func first<T>(items: &[T]): T? {
    ...
}

Generic parameter grammar in v0:

GenericParameters = "<" GenericParameter ("," GenericParameter)* ">"
GenericParameter  = Name

Generic bounds are deferred after v0. The parser must diagnose a generic parameter colon such as T: Printable as a deferred feature.

Generic implementation uses monomorphization. Each concrete instantiation is compiled as concrete code.

Buffer<i32>
Buffer<String>

This keeps generic dispatch static, avoids runtime type metadata for basic generics, and fits the no-runtime direction.

Initial generic scope:

  • type parameters on structs
  • type parameters on functions
  • type parameters on impl blocks where needed
  • compile-time monomorphization

Deferred generic features:

  • inline bounds such as T: Printable
  • multiple bounds such as T: A + B
  • full where clauses
  • higher-kinded types
  • generic associated types
  • const generics beyond the minimum needed for fixed-size arrays

Embedding

Adopted future design: embedding is Nocter's privacy-preserving composition feature.

Embedding declares that a struct stores an unnamed value of another struct type. The embedding owner may use only the embedded type's public surface. Private fields, private methods, private associated functions, and other implementation details of the embedded type are not promoted, even when the embedding owner is declared in the same module.

Initial syntax:

struct Profile {
    ...User
    pub ...Article

    visits: i32
}

In the embedding subset, leading ... is recognized only in struct bodies and struct literals. Broader future uses of ... are defined separately in Future Literal Definitions and Spread so embedding does not become a hidden import, macro, or general delegation mechanism.

Meaning:

  • ...User stores a User value and promotes User's public instance members to the Profile implementation scope only.
  • pub ...Article stores an Article value and promotes Article's public instance members as public Profile members.
  • The embedded values do not receive source-level field names such as user, article, User, or Article.
  • The embedded types do not know that they are embedded.

Embedding is directional. If Profile embeds User, Profile owns a User value. User does not gain access to Profile, does not see Profile's private or public members, and does not dispatch to Profile overrides. self inside a promoted User method is still the embedded User, not the outer Profile.

Example:

struct User {
    id: u64
    name: String
    pub age: i32
}

impl User {
    pub method &self.print(): void {
        print(self.name)
    }
}

struct Article {
    title: String
    pub text: String
}

struct Profile {
    ...User
    pub ...Article

    visits: i32
}

impl Profile {
    method &self.run(): void {
        self.print() // OK: User.print is public, promoted inside Profile
        self.id      // error: User.id is not public
        self.age     // OK: User.age is public, promoted inside Profile
        self.title   // error: Article.title is not public
        self.text    // OK: Article.text is public and promoted
        self.visits  // OK: Profile's own private field
    }
}

Outside Profile's implementation scope:

let profile = make_profile()

profile.print()  // error: User was embedded privately
profile.id       // error: User.id is private to User
profile.age      // error: User was embedded privately
profile.title    // error: Article.title is private to Article
profile.text     // OK: Article was embedded publicly and text is public
profile.visits   // error: visits is private to Profile

Embedded Initialization

Struct literals initialize embedded values with embedded initializers:

func Profile.new(
    name: String,
    age: i32,
    title: String,
    text: String,
): Profile {
    return Profile{
        ...User.new(move name, age),
        ...Article.new(move title, move text),
        visits: 0,
    }
}

Rules:

  • An embedded initializer has the form ...expr.
  • expr must have the embedded target type after generic substitution.
  • Every embedding declaration must be initialized exactly once.
  • Unknown embedded initializers are compile errors.
  • Duplicate embedded initializers are compile errors.
  • Field initializers and embedded initializers are evaluated left to right in the order written in the struct literal.
  • If initialization fails through postfix ?, already initialized ordinary fields and embedded values are dropped in reverse initialization order.
  • A struct may not embed the same concrete target type more than once in the initial design.

Promotion And Visibility

Embedding promotion is not ordinary import, not ordinary module-private access, and not a named field.

Only the embedded type's public instance members are promotable:

  • public fields
  • public &self receiver methods
  • public &+self receiver methods

Not promoted in the initial design:

  • private fields
  • private methods
  • associated functions
  • drop members
  • enum variants
  • type aliases
  • nested declarations
  • consuming self receiver methods
  • pub(nocter) members

Consuming receiver methods are not promoted initially because calling one would partially move the embedding owner. A future design may allow them only after partial-move and drop-state rules for embedded values are fully specified.

For ...T, promoted members are visible only inside the embedding owner's implementation scope:

  • inherent methods in impl Owner
  • drop &+self in impl Owner
  • qualified associated functions declared as func Owner.name

They are not visible to unrelated top-level functions in the same module. This is stricter than ordinary module-private visibility because embedding is a type-contract boundary.

For pub ...T, promoted members become public members of the embedding owner. They are available anywhere the owner type is visible and the promoted member's use satisfies normal borrow, assignment, and ownership rules. Reading or writing a promoted public field reads or writes the field inside the embedded value.

Name Collisions

Embedding does not provide renaming, aliasing, override order, or explicit disambiguation.

A promoted member name must not collide with:

  • an ordinary field declared directly by the embedding owner
  • an inherent method declared directly by the embedding owner
  • a member promoted by another embedding
  • another member promoted by the same embedding target

Collisions are compile errors regardless of whether the colliding members would be private or public after promotion.

Rationale: a collision means the composed API has stopped being self-evident. Nocter requires the author to choose clearer names at the source types instead of adding local rename syntax.

Interface Interaction

Embedding does not create implicit interface conformance.

pub interface Printable {
    pub method &self.print(): i32
}

struct User {
    id: u64
}

impl User {
    pub method &self.print(): i32 {
        return 0
    }
}

struct Profile {
    pub ...User
}

impl Printable for Profile // explicit conformance is still required

When checking an explicit impl Interface for Owner, public methods promoted by pub ...T may satisfy interface requirements as public Owner methods. Methods promoted by private ...T cannot satisfy a public interface because they are not public on Owner.

Non-Goals

Embedding is not:

  • class inheritance
  • subclassing
  • trait implementation reuse
  • interface default methods
  • mixins
  • extension methods
  • implicit conversion
  • automatic delegation to private implementation details

Class inheritance is not part of the core language direction. Interface default methods and trait-style code reuse are not part of the core language direction.