Skip to content

Latest commit

 

History

History
1317 lines (1032 loc) · 38.8 KB

File metadata and controls

1317 lines (1032 loc) · 38.8 KB

Dynace-RS Design Document

This document describes the design of the Dynace-RS object system as actually implemented. The system has been built in full; this file is the authoritative specification and reference for what exists and how it fits together.

A small set of features from the original intent is intentionally deferred — see Known limitations at the end.

Purpose

A Rust crate family that implements a Dynace-like dynamic object system on top of Rust.

The design is inspired by Dynace and Smalltalk:

  • Classes and instances are both ordinary objects.
  • A class object defines the layout and behavior of its instances.
  • A metaclass object defines the layout and behavior of class objects.
  • A metametaclass object defines the layout and behavior of metaclass objects.
  • Instance variables are defined by classes and stored in instances.
  • Class variables are defined by metaclasses, stored on the declaring class, and shared across the hierarchy.
  • Class variables are inherited through the metaclass hierarchy.
  • Instance variables are inherited through the class hierarchy.
  • The metaclass hierarchy parallels the class hierarchy.
  • Generic functions form the public interface.
  • Methods are normally private to their defining class.
  • Generics are generated automatically from methods plus a generics database.
  • Normal Rust code does not use string class names.

This is not intended to replace Rust traits. It is an embedded dynamic object world implemented as a Rust runtime plus macros and code generation tools.

Workspace Layout

Dynace-RS/
  Cargo.toml                 (workspace; default-members excludes examples)
  Dynace-RS_design.md        (this file)
  Plan.md                    (development plan + status)
  crates/
    dynace/                  (runtime + reflection)
      Cargo.toml
      src/
        lib.rs
        symbol.rs            (Symbol + global interner)
        error.rs             (Error + Result)
        value.rs             (Value enum)
        object.rs            (ObjectRef, ObjectHeader, ObjectFlags)
        payload.rs           (ObjectCell, ObjectPayload, ObjectKind, …)
        slots.rs             (InstanceVarDef, ClassVarDef, …)
        method.rs            (Selector, GenericId, MethodDef, MethodFn, …)
        registry.rs          (Registry — markers → ObjectRefs)
        marker.rs            (DynaceClassMarker / DynaceMetaclassMarker traits +
                              ObjectClass / ObjectMetaClass / MetaMetaClass)
        macro_support.rs     (ClassMetadata / MethodMetadata for generator)
        world.rs             (World, ClassDef, bootstrap, dispatch, …)
        reflect.rs           (reflection API on World)
      tests/                 (integration tests — see Testing section)
    dynace-macros/           (proc macros: class!, method!, register!)
      Cargo.toml             (proc-macro = true; deps: syn, quote, proc-macro2)
      src/
        lib.rs
        class_macro.rs
        method_macro.rs
        register_macro.rs
    dynace-gen/              (scanner + generator; lib + CLI)
      Cargo.toml             (deps: syn, quote, toml, serde, walkdir)
      src/
        lib.rs               (run(GenerateConfig) pipeline)
        main.rs              (CLI)
        metadata.rs          (intermediate ClassMeta / MethodMeta)
        scanner.rs           (syn-based scan of statics + macro invocations)
        macro_input.rs       (re-parses class!/method! macro bodies)
        database.rs          (TOML I/O)
        merge.rs             (merge methods → generics, conflict detection)
        emit.rs              (Rust wrapper emission)
      tests/end_to_end.rs
    dynace-build/            (build-script helper API)
      Cargo.toml             (depends on dynace-gen)
      src/lib.rs
  examples/                  (one self-contained crate per example;
                              NOT in default-members)
    01-hello-object/
    02-define-a-class/
    03-instance-variables/
    04-class-variables/
    05-single-inheritance/
    06-multiple-inheritance/
    07-private-methods/
    08-class-side-methods/
    09-polymorphism/
    10-generated-generics/
    11-reflection/
    12-mini-shape-system/
  docs/
    dynace-tutorial.texi
    dynace-manual.texi
    version.texi
    dynace.css
    Makefile                  (targets: html / pdf / all / clean)
    build/                    (generated; .gitignored)

Workspace default-members lists only the four library crates, so cargo build and cargo test from the workspace root touch the library crates only. Each example is built individually with cargo run -p example-NN-name, or via the Makefile inside its directory.

High-Level Architecture

The system has three layers:

  1. Runtime object layer (the dynace crate)

    • Object handles, headers, classes, slot storage, method tables.
    • Generic dispatch.
    • Reflection.
  2. Macro layer (the dynace-macros crate, re-exported through dynace)

    • class! — declares a class and (auto-derived) metaclass.
    • method! — declares one or more methods on a class or metaclass marker.
    • register! — batches Type::register / Type::register_method_X calls.
  3. Generator layer (the dynace-gen library + binary, and the dynace-build build-script helper)

    • Scans the current crate's source for class!/method! macro invocations and for the metadata statics they emit.
    • Reads optional TOML generic databases from upstream systems.
    • Merges everything and emits one normalized TOML database plus one Rust source file of typed generic wrappers.

No String Class Names in Normal Code

Normal code refers to classes by Rust identifiers, not strings.

Acceptable:

let obj = world.new_instance_typed::<DogClass>()?;
let cls = obj.class_object(&world)?;
let capacity = world.get_class_var(cls, "Capacity")?;

Acceptable macro syntax:

dynace::class! {
    class DogClass : AnimalClass {
        instance_vars { name: RustString, age: Int }
        class_vars    { Population: Int }
    }
}

Not acceptable as normal code:

let obj = new!("DogClass", &[]);                 // string class name
let obj = world.new_instance_by_name("DogClass") // not exposed

String lookup may exist only for:

  • Reflection
  • Debugging
  • Loading external databases
  • Compatibility tools

(Variable and method names are strings, by deliberate choice — see Variables below.)

Core Object Model

Objects

Every runtime entity is an object:

  • Ordinary instances
  • Class objects
  • Metaclass objects
  • The metametaclass

An object has:

pub struct ObjectHeader {
    pub class: ObjectRef,
    pub flags: ObjectFlags,
}

The class field points to the object's defining class object. For a metametaclass, that field is self-referential.

ObjectRef

Opaque handle:

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ObjectRef(NonZeroU64);

impl ObjectRef {
    pub fn id(self) -> u64;
    pub fn from_id(id: u64) -> Option<Self>;
    pub fn class_object(self, world: &World) -> Result<ObjectRef>;
}

The numeric id is heap-index + 1; the NonZeroU64 lets Option<ObjectRef> be the same size as ObjectRef. from_id is for deserialization and can produce a syntactically valid reference that does not address a real cell; operations on such a reference return Error::InvalidObjectRef.

Object kind

pub enum ObjectKind {
    Ordinary,
    Class,
    Metaclass,
    MetaMetaclass,
    RustNative,
}

world.reflect_kind(obj) classifies any object.

Object Allocation

World owns the heap. The MVP uses Vec<ObjectCell>:

pub struct ObjectCell {
    pub header: ObjectHeader,
    pub payload: ObjectPayload,
}

Possible future evolutions: slotmap-based heap, reference counting, tracing GC, thread-safe heaps. None are required for the current scope.

Object Payloads

pub enum ObjectPayload {
    Ordinary(OrdinaryPayload),
    Class(ClassPayload),
    Metaclass(MetaclassPayload),
    MetaMetaclass(MetaMetaclassPayload),
    RustNative(Box<dyn Any + Send + Sync>),
}

This enum is an implementation detail. Semantically every variant is still an object and goes through the same class_of / call_generic / reflection paths.

Classes, Metaclasses, and Metametaclass

Class Objects

pub struct ClassPayload {
    pub name: Symbol,
    pub class_id: ClassId,
    pub supers: Vec<ObjectRef>,
    pub cpl: Vec<ObjectRef>,
    pub local_instance_vars: Vec<InstanceVarDef>,
    pub instance_layout: InstanceLayout,
    pub class_var_values: SlotStorage,
    pub methods: MethodTable,
}

A class object is not merely metadata; it is an object that receives messages.

Metaclass Objects

pub struct MetaclassPayload {
    pub name: Symbol,
    pub class_id: ClassId,
    pub supers: Vec<ObjectRef>,
    pub cpl: Vec<ObjectRef>,
    pub local_class_vars: Vec<ClassVarDef>,
    pub class_var_layout: ClassVarLayout,
    pub methods: MethodTable,
    pub instance_class: Option<ObjectRef>,
}

Rule:

Class variables are declared on metaclasses, stored on the class whose
metaclass declared them, and shared by all descendants of that class.

Lookup walks the receiver class's CPL until it finds a class whose metaclass declares the variable locally; reads and writes then go to that class's storage. Two subclasses inheriting the same class variable therefore see one shared slot.

Metametaclass

pub struct MetaMetaclassPayload {
    pub name: Symbol,
    pub supers: Vec<ObjectRef>,
    pub cpl: Vec<ObjectRef>,
    pub methods: MethodTable,
}

World::new bootstraps exactly one metametaclass; its class field points to itself.

Smalltalk-Style Hierarchy Invariant

The metaclass hierarchy parallels the class hierarchy.

If DogClass : AnimalClass, then metaclass(DogClass) : metaclass(AnimalClass). For multiple inheritance the same rule applies in declaration order: CClass : AClass, BClass implies metaclass(CClass) : metaclass(AClass), metaclass(BClass).

The runtime enforces this at define_class time. If the caller passes explicit metaclass_supers that don't match class_of(s) for each class super, the runtime returns Error::InconsistentInheritance.

Multiple Inheritance

Multiple inheritance uses C3 linearization for both the class hierarchy and the metaclass hierarchy.

Given a new class with direct supers [P1, P2, …, Pn]:

CPL(C) = [C] ++ merge(CPL(P1), CPL(P2), …, CPL(Pn), [P1, P2, …, Pn])

The merge step repeatedly picks a "good head" — the first element of one of the lists that does not appear in the tail of any other list. If no good head can be selected while lists remain non-empty, the hierarchy is inconsistent and Error::InconsistentInheritance is returned.

The class precedence list (CPL) drives:

  • instance-variable inheritance
  • method lookup
  • reflection
  • is_kind_of

The metaclass CPL drives:

  • class-variable inheritance
  • class-side method lookup
  • reflection of class-side behavior

Field conflict rules

  • Diamond inheritance. Two CPL ancestors contributing the same (name, declaring_class) produce one slot in the descendant's layout — the shared logical variable.
  • Ambiguity. Two CPL ancestors contributing the same name but different declaring_class values produce Error::AmbiguousSlot.
  • Silent shadowing rejected. A locally declared variable whose name matches an inherited variable produces Error::InconsistentInheritance. Explicit override syntax (e.g. override var name) is documented as a future feature; see Known limitations.

Instance Variables

Instance variables are:

  • declared by classes,
  • inherited through the class CPL,
  • stored on each instance.
pub struct InstanceVarDef {
    pub name: Symbol,
    pub ty: TypeDescriptor,
    pub visibility: SlotVisibility,
    pub declaring_class: ObjectRef,
    pub offset: SlotIndex,
}

pub struct InstanceVarSpec {
    pub name: Symbol,
    pub ty: TypeDescriptor,
    pub visibility: SlotVisibility,
}

InstanceVarSpec is what the user passes to define_class (typically through the class! macro). InstanceVarDef is what the runtime stores in local_instance_vars/instance_layout. The runtime fills in declaring_class and offset.

Storage:

pub struct OrdinaryPayload {
    pub slots: SlotStorage,
}

Slot storage is sized to the class's full layout and initialized to Value::Nil.

Required API

impl World {
    pub fn get_instance_var(
        &self,
        obj: ObjectRef,
        name: impl Into<Symbol>,
    ) -> Result<Value>;

    pub fn set_instance_var(
        &mut self,
        obj: ObjectRef,
        name: impl Into<Symbol>,
        value: Value,
    ) -> Result<()>;
}

Class Variables

Class variables are:

  • declared by metaclasses (in source: written in the class block via class_vars { … }, which the macro routes to the auto-generated metaclass),
  • inherited through the metaclass CPL,
  • stored on the class whose metaclass declared them,
  • shared by every descendant of the declaring class — there is one storage cell per declared variable, not one per subclass,
  • never stored on instances.
pub struct ClassVarDef {
    pub name: Symbol,
    pub ty: TypeDescriptor,
    pub visibility: SlotVisibility,
    pub declaring_metaclass: ObjectRef,
    pub offset: SlotIndex,
}

pub struct ClassVarSpec {
    pub name: Symbol,
    pub ty: TypeDescriptor,
    pub visibility: SlotVisibility,
}

A class's class_var_values storage is sized to its own local_class_vars.len() — the variables this class declared, no more. When a subclass reads or writes an inherited variable, the runtime walks the subclass's CPL, finds the class that declared it locally, and routes the access to that class's storage. Two siblings looking up the same inherited variable therefore see the same slot; a write through one is visible through the other.

A ClassVarDef's offset is the slot index within the declaring class's class_var_values (i.e. the position within the declaring metaclass's local_class_vars), not a position in any combined layout.

Required API

impl ObjectRef {
    pub fn class_object(self, world: &World) -> Result<ObjectRef>;
}

impl World {
    pub fn class_of(&self, obj: ObjectRef) -> Result<ObjectRef>;

    pub fn get_class_var(
        &self,
        class_obj: ObjectRef,
        name: impl Into<Symbol>,
    ) -> Result<Value>;

    pub fn set_class_var(
        &mut self,
        class_obj: ObjectRef,
        name: impl Into<Symbol>,
        value: Value,
    ) -> Result<()>;
}

Headline example:

let dog       = world.new_instance_typed::<DogClass>()?;
let dog_class = world.class_of(dog)?;
world.set_class_var(dog_class, "Population", 100.into())?;
let pop       = world.get_class_var(dog_class, "Population")?;

Method Model

Methods are normally private to their defining class.

pub type MethodFn = fn(&mut World, ObjectRef, &[Value]) -> Result<Value>;

pub struct MethodDef {
    pub id: MethodId,
    pub selector: Selector,
    pub defining_class: ObjectRef,
    pub visibility: MethodVisibility,
    pub function: MethodFn,
    pub signature: MethodSignature,
}

pub enum MethodVisibility {
    Private,
    GenericExported,
    InternalRuntime,
}

Default visibility is Private. A method becomes part of the public interface only when it is reachable through a generated generic.

Visibility semantics:

  • Private — implementation is private to its defining class. The generator emits a public generic wrapper that dispatches to it (because being reachable through the generic is what makes a method callable from outside the class).
  • GenericExported — explicitly part of the public interface. Same effect as Private for the generator today; the variant is there for reflection and for future tooling that wants to draw the distinction.
  • InternalRuntime — runtime-only. The generator deliberately does not emit a public generic wrapper for methods marked this way; they are invisible to user code that calls only the generated generics. Useful for bootstrap helpers and internal primitives registered via register_method_with.

Programmatic registration

The method! macro is the normal path; for programmatic use:

impl World {
    pub fn register_method(
        &mut self,
        class_ref: ObjectRef,
        selector: impl Into<Selector>,
        function: MethodFn,
    ) -> Result<MethodId>;

    pub fn register_method_with(
        &mut self,
        class_ref: ObjectRef,
        selector: impl Into<Selector>,
        visibility: MethodVisibility,
        signature: MethodSignature,
        function: MethodFn,
    ) -> Result<MethodId>;
}

class_ref may be a class object, a metaclass object, or the metametaclass — the runtime handles all three through one path.

Generics

Core rule

Generic names are the same as the method names they dispatch to. If Class1 and Class2 both define a method M1, there is one public generic named M1. Calling the generic dispatches to the method belonging to the receiver's class.

let a = world.new_instance_typed::<Class1>()?;
let b = world.new_instance_typed::<Class2>()?;

M1(&mut world, a, &[])?; // dispatches to Class1's M1
M1(&mut world, b, &[])?; // dispatches to Class2's M1

The M1 Rust function in the snippet above is generated by dynace-gen, not hand-written. The runtime equivalent is world.call_generic(GenericId::from_static("M1"), receiver, &[])?.

Generics are not hand-written

The expected workflow is:

  1. Declare classes and methods with the macros.
  2. A build script invokes dynace-build (which calls dynace-gen) during compilation.
  3. The generator scans the crate source and emits the typed wrappers into $OUT_DIR/dynace_generics.rs.
  4. The crate brings them in via include!(concat!(env!("OUT_DIR"), "/dynace_generics.rs")).

Public interface rule

Methods are private implementation details. Generics form the public interface. Class-local methods are not exposed directly as ordinary public Rust functions unless they are:

  • constructors,
  • bootstrap or runtime internals,
  • explicitly marked internal,
  • reflection or debug helpers.

Normal user-facing operations look like:

M1(&mut world, obj, &[args])

rather than:

Class1::M1(&mut world, obj, &[args])
obj.M1()

Dispatch

Single-dispatch on the receiver:

pub fn call_generic(
    &mut self,
    generic: GenericId,
    receiver: ObjectRef,
    args: &[Value],
) -> Result<Value>;

The runtime:

  1. Resolves class = class_of(receiver).
  2. Walks CPL(class) looking for a method whose selector matches the generic.
  3. Invokes the first match.
  4. Caches the result keyed on (class, selector).

Generated constants

For every generic, the generator emits:

#[allow(non_upper_case_globals)]
pub const GENERIC_M1: ::dynace::GenericId = ::dynace::GenericId::from_static("M1");

GenericId wraps &'static str so it is const-constructible.

Generic metadata

World::reflect_generics() returns a cross-class index:

pub struct ReflectedGeneric {
    pub selector: Selector,
    pub implementations: Vec<ReflectedGenericImpl>,
}

pub struct ReflectedGenericImpl {
    pub class: ObjectRef,
    pub class_name: Symbol,
    pub method_id: MethodId,
    pub visibility: MethodVisibility,
    pub arity: usize,
}

The generator emits a parallel index into the TOML database file.

Selectors

#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct Selector(pub Symbol);

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct GenericId(&'static str);

impl GenericId {
    pub const fn from_static(name: &'static str) -> Self;
    pub fn name(self) -> &'static str;
    pub fn as_selector(self) -> Selector;
}

Selectors are interned symbols, used internally as method-table keys. GenericId carries a &'static str so generated code can write pub const GENERIC_X: GenericId = GenericId::from_static("X") at the crate level.

Selectors must be valid Rust identifiers — see Known limitations for the not-yet-implemented safe-wrapper rule.

Text-Based Generics Database

The generator writes one normalized TOML database per run. The format matches the original spec:

[[generic]]
name = "M1"
selector = "M1"
arity = 0
receiver_position = 0
visibility = "public"

[[generic.implementation]]
class = "Class1"
method = "M1"
source = "src/class1.rs"

[[generic.implementation]]
class = "Class2"
method = "M1"
source = "src/class2.rs"

Output is sorted deterministically: generics by selector, each generic's implementations by (class, method).

Merge rules

The generator:

  1. Loads each input TOML database.
  2. Scans configured source directories.
  3. Merges by selector. Identical (class, method) entries are deduplicated.
  4. Detects arity mismatches and returns dynace_gen::Error::SignatureConflict { selector, existing_arity, discovered_arity }.
  5. Preserves source paths.
  6. Sorts and writes.

Scanner inputs

The scanner reads:

  • Macro-emitted metadata statics (__DYNACE_CLASS_META_* and __DYNACE_METHOD_META_*).
  • class! and method! macro invocations directly. (The scanner re-uses the same grammar the proc macros parse, so it sees metadata even when macro expansion has not yet run on the source.)

The inventory/linkme and "explicit build-script declarations" paths mentioned in earlier drafts are not implemented; see Known limitations.

Generated Rust file

The generator emits, per selector:

#[allow(non_upper_case_globals)]
pub const GENERIC_AT_PUT: ::dynace::GenericId = ::dynace::GenericId::from_static("AtPut");

#[allow(non_snake_case)]
pub fn AtPut(
    world: &mut ::dynace::World,
    receiver: ::dynace::ObjectRef,
    args: &[::dynace::Value],
) -> ::dynace::Result<::dynace::Value> {
    world.call_generic(GENERIC_AT_PUT, receiver, args)
}

All paths are fully qualified (::dynace::*) and every item carries its own #[allow] attribute, so the file can be include!d into any module without colliding with the parent's imports or attributes.

The const name is GENERIC_<UPPER> where the selector is converted to SHOUTY_SNAKE (AtPutAT_PUT, HTMLDocHTML_DOC).

Class and Method Macros

class!

dynace::class! {
    class AnimalClass : ObjectClass {
        instance_vars {
            name: RustString,
            age: Int,
        }
        class_vars {
            Population: Int,
        }
    }
}

Multiple inheritance:

dynace::class! {
    class CClass : AClass, BClass {
        instance_vars { local: Int }
    }
}

Optional explicit metaclass block (for fine-grained control — rarely needed):

dynace::class! {
    class CClass : AClass, BClass {
        instance_vars { local: Int }
    }

    metaclass CMetaClass : AMetaClass, BMetaClass {
        class_vars { Count: Int }
    }
}

If the metaclass block is omitted, the macro generates a parallel metaclass automatically. The auto-derived name follows the rule FooClassFooMetaClass: the Class suffix is replaced. If the class name does not end in Class, MetaClass is appended.

Per-class, the macro emits:

  • pub struct ClassName; and pub struct ClassNameMetaClass; (or the user's explicit metaclass marker name).
  • impl DynaceClassMarker for ClassName {} and impl DynaceMetaclassMarker for ClassNameMetaClass {}.
  • impl ClassName { pub fn register(&mut World) -> Result<(ObjectRef, ObjectRef)> }.
  • #[doc(hidden)] pub static __DYNACE_CLASS_META_ClassName: ClassMetadata.

Class variables may appear in either the class block (preferred — keeps the metaclass concept invisible) or the explicit metaclass block, but not both at once. The latter is an error.

method!

dynace::method! {
    impl Class1 {
        fn M1(world: &mut World, receiver: ObjectRef) -> Result<Value> {
            Ok(Value::Nil)
        }
    }
}

The impl target is the class marker for an instance-side method, or the metaclass marker for a class-side method:

dynace::method! {
    impl Class1MetaClass {
        fn New(world: &mut World, receiver_class: ObjectRef) -> Result<Value> {
            let obj = world.new_instance(receiver_class)?;
            Ok(Value::Object(obj))
        }
    }
}

The body may be written with two or three arguments. The two-arg form (world, receiver) is wrapped to ignore the args slice; three-arg (world, receiver, args) is forwarded as written.

Per method, the macro emits:

  • A private mangled function carrying the body. No public Rust function named after the selector is emitted — the public callable is the generated generic.
  • impl Target { pub fn register_method_<Name>(&mut World) -> Result<MethodId> }.
  • #[doc(hidden)] pub static __DYNACE_METHOD_META_Target_Name: MethodMetadata.

register!

Convenience that batches register / register_method_<Name> calls. Each entry is a path: one-segment paths register classes, two-segment paths register a method:

dynace::register!(&mut world,
    AnimalClass,
    DogClass,
    CatClass,
    AnimalClass::Speak,
    DogClass::Speak,
);

Class registrations are emitted before method registrations regardless of listing order. The macro propagates errors via ?, so the enclosing function must return a Result.

Dispatch Semantics

Instance-side

M1(&mut world, obj, &[])

The runtime:

  1. class = class_of(obj).
  2. Walks CPL(class).
  3. Searches each class for M1 in its method table.
  4. Invokes the first match.

Class-side

New(&mut world, DogClass::class_object(&world)?, &[])

The runtime:

  1. class = class_of(DogClassObject) — which is DogMetaClass.
  2. Walks CPL(DogMetaClass).
  3. Searches metaclasses for New.
  4. Invokes the first match.

Same path

Instances, classes, metaclasses, and the metametaclass all go through:

world.call_generic(generic_id, receiver, args)

There is no separate API surface for class-side calls.

Reflection

Reflection is read-only and deterministic. All methods are on World and prefixed reflect_.

Object classification

  • reflect_kind(obj) -> Result<ObjectKind>
  • reflect_name(class) -> Result<Symbol>
  • reflect_all_classes() -> Vec<ObjectRef>
  • reflect_all_metaclasses() -> Vec<ObjectRef>
  • reflect_all_metametaclasses() -> Vec<ObjectRef>
  • reflect_all_ordinary() -> Vec<ObjectRef>

Hierarchy

  • reflect_supers(class) -> Result<Vec<ObjectRef>>
  • reflect_cpl(class) -> Result<Vec<ObjectRef>>

Variables

  • reflect_instance_vars(class) -> Result<Vec<InstanceVarDef>> — full layout, offset order.
  • reflect_local_instance_vars(class) -> Result<Vec<InstanceVarDef>>
  • reflect_class_vars(metaclass) -> Result<Vec<ClassVarDef>>
  • reflect_local_class_vars(metaclass) -> Result<Vec<ClassVarDef>>
  • reflect_class_var_values(class_obj) -> Result<Vec<(Symbol, Value)>> — the actual stored values on a class object, in layout order.

Methods

  • reflect_local_methods(class) -> Result<Vec<MethodDef>> — sorted by selector. Does not follow inheritance; use call_generic or lookup_method_def for inheritance-aware lookup.

Generics

  • reflect_generics() -> Vec<ReflectedGeneric> — one entry per selector, sorted by selector; each entry's implementations sorted by class id.
  • reflect_generic_implementations(selector) -> Vec<ReflectedGenericImpl>

Registry

The registry is an implementation index, not the semantic owner of classes.

pub struct Registry {
    classes_by_type:     HashMap<TypeId, ObjectRef>,
    metaclasses_by_type: HashMap<TypeId, ObjectRef>,
    classes_by_name:     HashMap<Symbol, ObjectRef>,
    metaclasses_by_name: HashMap<Symbol, ObjectRef>,
}

Lookup APIs:

impl Registry {
    pub fn class_by_type_id(&self, id: TypeId) -> Option<ObjectRef>;
    pub fn metaclass_by_type_id(&self, id: TypeId) -> Option<ObjectRef>;
    pub fn class_by_name(&self, name: Symbol) -> Option<ObjectRef>;
    pub fn metaclass_by_name(&self, name: Symbol) -> Option<ObjectRef>;
}

impl World {
    pub fn registry(&self) -> &Registry;
    pub fn resolve_marker_object<T: 'static>(&self) -> Result<ObjectRef>;
}

resolve_marker_object<T> tries both the class and metaclass tables, which is how the method! macro accepts class or metaclass markers uniformly.

For scripting / deserialization paths, the runtime also exposes by-name lookup directly on World:

impl World {
    pub fn class_by_name(&self, name: impl Into<Symbol>) -> Result<ObjectRef>;
    pub fn metaclass_by_name(&self, name: impl Into<Symbol>) -> Result<ObjectRef>;
    pub fn new_instance_by_name(&mut self, name: impl Into<Symbol>) -> Result<ObjectRef>;
}

These are intended for reflection, debugging, and loading external data where the class identity arrives as a string. Normal code should still go through typed markers (MyClass::class_object(&world)).

Typed Class Markers

Each class macro generates a zero-sized marker type and trait impl:

pub struct DogClass;
pub struct DogMetaClass;

impl DynaceClassMarker for DogClass {}
impl DynaceMetaclassMarker for DogMetaClass {}

Trait shapes (default impls look the marker up in the registry):

pub trait DynaceClassMarker: Any {
    fn class_object(world: &World) -> Result<ObjectRef> where Self: Sized;
}

pub trait DynaceMetaclassMarker: Any {
    fn metaclass_object(world: &World) -> Result<ObjectRef> where Self: Sized;
}

Construction:

let dog = world.new_instance_typed::<DogClass>()?;

Class object access:

let dog_class = DogClass::class_object(&world)?;

The root markers ObjectClass, ObjectMetaClass, and MetaMetaClass are defined by the runtime crate and wired up by World::new.

Values

pub enum Value {
    Nil,
    Bool(bool),
    Int(i64),
    Float(f64),
    String(String),
    Object(ObjectRef),
}

From impls cover bool, i32, i64, f64, &str, String, and ObjectRef. Symbols, arrays, byte arrays, and arbitrary Rust values may be added later.

Error Type

pub enum Error {
    UnknownClass(Symbol),
    UnknownGeneric(Symbol),
    UnknownMethod { selector: Symbol, class: ObjectRef },
    InconsistentInheritance { class: Symbol, reason: String },
    AmbiguousSlot { name: Symbol },
    SlotNotFound { name: Symbol },
    ClassVariableNotFound { name: Symbol, class: ObjectRef },
    InstanceVariableNotFound { name: Symbol, class: ObjectRef },
    InvalidReceiver,
    TypeMismatch,
    InvalidObjectRef(ObjectRef),
    DuplicateClassMarker(Symbol),
    GenericSignatureConflict {
        generic: Symbol,
        existing: MethodSignature,
        discovered: MethodSignature,
    },
    BootstrapError(String),
}

pub type Result<T> = std::result::Result<T, Error>;

GenericSignatureConflict is raised at register_method_with time when the new method's MethodSignature (currently just arity) disagrees with an existing implementation of the same selector elsewhere in the world. The same shape is mirrored by dynace_gen::Error::SignatureConflict, which is raised by the generator during database-merge.

Build Workflow

A downstream crate uses a build script:

fn main() {
    dynace_build::generate_generics()
        .scan_crate("src")
        .out_dir(std::env::var("OUT_DIR").unwrap())
        .run()
        .unwrap();
}

And then include!s the result:

include!(concat!(env!("OUT_DIR"), "/dynace_generics.rs"));

The builder API in dynace-build:

impl GenerateGenerics {
    pub fn scan_crate<P: AsRef<Path>>(self, path: P) -> Self;
    pub fn input_database<P: AsRef<Path>>(self, path: P) -> Self;
    pub fn out_dir<P: AsRef<Path>>(self, path: P) -> Self;
    pub fn out_database<P: AsRef<Path>>(self, path: P) -> Self;
    pub fn out_rust<P: AsRef<Path>>(self, path: P) -> Self;
    pub fn run(self) -> Result<RunReport, Box<dyn std::error::Error>>;
}

run() emits cargo:rerun-if-changed= lines so changes to scanned sources or input databases trigger rebuilds.

The standalone CLI dynace-gen accepts the same inputs via --scan, --input-db, --out-dir, --out-database, --out-rust flags.

Default output paths:

  • <out_dir>/generics.generated.toml
  • <out_dir>/dynace_generics.rs

Testing

Integration tests live in crates/dynace/tests/:

  • tests/object_model.rs — bootstrap roots, class-of spine, object kinds.
  • tests/class_creation.rsdefine_class, typed markers, parallel hierarchy enforcement, CPL chaining.
  • tests/instance_vars.rs — instance variable storage, inheritance, shadowing rejection.
  • tests/class_vars.rs — class-var schema in metaclass, storage in class object, independent sibling storage.
  • tests/multiple_inheritance.rs — C3 linearization for class and metaclass hierarchies, diamond inheritance, ambiguity errors, inconsistent-hierarchy rejection, is_kind_of.
  • tests/methods.rs — registration, CPL-walking dispatch, override semantics, class-side dispatch, cache.
  • tests/macros.rs — end-to-end class!/method!/register! usage.
  • tests/reflection.rs — every reflection method against a small fixture world.

Plus unit tests in crates/dynace/src/{symbol.rs, world.rs} (Symbol interner, C3 merge) and crates/dynace-gen/tests/end_to_end.rs (scanner, merge, emit, build-the-output-with-rustc).

Examples

Each example is a self-contained Cargo crate under examples/NN-name/, with its own Cargo.toml, src/main.rs, Makefile (targets build / run / clean), and readme.txt. Each Cargo.toml is fully self- contained (no workspace inheritance) so the user can copy the directory anywhere on disk and build after editing one dependency path.

Dir Demonstrates
01-hello-object First class + method + dispatch
02-define-a-class Typed markers, class_of
03-instance-variables Per-instance storage
04-class-variables Class-wide shared state
05-single-inheritance Inherited ivars + cvars
06-multiple-inheritance Diamond, C3, is_kind_of
07-private-methods Method dispatch + override
08-class-side-methods Advanced: class-side methods (first place metaclasses surface)
09-polymorphism One generic, many class implementations
10-generated-generics Advanced: build.rs + dynace-build + generated wrapper
11-reflection Advanced: full reflection dump
12-mini-shape-system Capstone: MI + ivars + cvars + class-side New + dispatch + reflection

Examples 01–07, 09, 10, and 12 never mention the metaclass concept. Metaclasses surface only in the explicitly-flagged advanced examples 08 and 11.

Documentation

Texinfo sources under docs/:

  • dynace-tutorial.texi — progressive hands-on guide, mirroring the example progression.
  • dynace-manual.texi — reference manual organized by concept, with a full error catalogue and design-rationale appendix.
  • version.texi — shared @set VERSION etc.
  • dynace.css — minimal stylesheet for the HTML output.
  • Makefile — targets html, pdf, all, clean. Outputs land in docs/build/{html,pdf}/.

The tutorial keeps metaclasses out of the basic chapters and introduces them only in the advanced "class methods" chapter. The manual covers metaclasses head-on in the concepts and class-definition chapters.

Building docs requires texinfo (for makeinfo and texi2pdf) plus a TeX distribution such as texlive-scheme-basic for PDF output.

Acceptance criteria

The system is acceptable when:

  1. Class names in normal code are Rust identifiers, not strings.
  2. Classes, metaclasses, and the metametaclass are ordinary objects.
  3. Ordinary instances can access their class object.
  4. Class variables are declared on metaclasses, stored on the declaring class, and shared by every descendant of that class.
  5. Instance variables are declared on classes and stored on instances.
  6. Instance variables inherit through the class CPL.
  7. Class variables inherit through the metaclass CPL.
  8. The metaclass hierarchy parallels the class hierarchy.
  9. Multiple inheritance works under C3 linearization.
  10. Methods are private to their defining class by default.
  11. Generic names equal their dispatched method names.
  12. Generics are generated, not hand-written.
  13. The generated generic is the public interface.
  14. The generated generic dispatches on the receiver's runtime class.
  15. Reflection exposes classes, metaclasses, variables, methods, and generics.
  16. All behaviour is covered by tests.

All sixteen criteria are met as of this writing (95 tests passing, 0 warnings).

Non-goals (intentionally out of scope)

  • Full tracing garbage collection.
  • Optimized inline caches for dispatch.
  • Multiple dispatch beyond receiver dispatch.
  • Dynamic mutation of a class's shape after instances of it have been created.
  • Network loading of class databases.
  • A full Smalltalk-style browser or IDE.

Known limitations

These are features described in earlier drafts of this document, or implied by the spec, that are not yet implemented. Each is straightforward to add without breaking existing APIs.

  1. Selectors with non-identifier characters. The original intent was: "If an external selector is not a valid Rust identifier, generate a safe Rust wrapper name and preserve the original selector in metadata." All current selectors must be valid Rust identifiers; the safe-wrapper-name pass is not yet wired up.

  2. Explicit override var name syntax. Silent shadowing of an inherited instance or class variable is rejected with Error::InconsistentInheritance. There is no syntax yet for explicit shadowing, which the original spec sketched as override var name.

  3. Explicit shared slot marker. Diamond inheritance (same logical variable reached via two CPL paths) collapses to one slot automatically. Two distinct ancestor classes contributing the same name produces Error::AmbiguousSlot. The originally sketched "explicitly marked as shared" path for the latter case is not implemented.

  4. inventory/linkme scanner input. The scanner reads macro-emitted metadata statics and class!/method! macro invocations. An alternate scanner input via the inventory or linkme crates is not wired up; macro-invocation scanning provides the same end-to-end capability.

  5. Richer MethodSignature. Method signatures currently track arity only. The runtime and the generator both use arity-based conflict detection. Tracking parameter and return types in MethodSignature is a future extension that the generator and runtime would consume.

  6. Richer TypeDescriptor. The current TypeDescriptor is a name wrapper. A real type system that the runtime could enforce on instance/class-variable writes is a future extension.

None of these gaps are blockers for the system's stated acceptance criteria above; they are extension points for future work.