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.
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.
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.
The system has three layers:
-
Runtime object layer (the
dynacecrate)- Object handles, headers, classes, slot storage, method tables.
- Generic dispatch.
- Reflection.
-
Macro layer (the
dynace-macroscrate, re-exported throughdynace)class!— declares a class and (auto-derived) metaclass.method!— declares one or more methods on a class or metaclass marker.register!— batchesType::register/Type::register_method_Xcalls.
-
Generator layer (the
dynace-genlibrary + binary, and thedynace-buildbuild-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.
- Scans the current crate's source for
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 exposedString lookup may exist only for:
- Reflection
- Debugging
- Loading external databases
- Compatibility tools
(Variable and method names are strings, by deliberate choice — see Variables below.)
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.
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.
pub enum ObjectKind {
Ordinary,
Class,
Metaclass,
MetaMetaclass,
RustNative,
}world.reflect_kind(obj) classifies any object.
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.
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.
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.
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.
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.
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 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
- 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
namebut differentdeclaring_classvalues produceError::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 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.
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 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.
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")?;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 asPrivatefor 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 viaregister_method_with.
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.
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 M1The 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, &[])?.
The expected workflow is:
- Declare classes and methods with the macros.
- A build script invokes
dynace-build(which callsdynace-gen) during compilation. - The generator scans the crate source and emits the typed wrappers into
$OUT_DIR/dynace_generics.rs. - The crate brings them in via
include!(concat!(env!("OUT_DIR"), "/dynace_generics.rs")).
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()Single-dispatch on the receiver:
pub fn call_generic(
&mut self,
generic: GenericId,
receiver: ObjectRef,
args: &[Value],
) -> Result<Value>;The runtime:
- Resolves
class = class_of(receiver). - Walks
CPL(class)looking for a method whose selector matches the generic. - Invokes the first match.
- Caches the result keyed on
(class, selector).
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.
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.
#[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.
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).
The generator:
- Loads each input TOML database.
- Scans configured source directories.
- Merges by selector. Identical
(class, method)entries are deduplicated. - Detects arity mismatches and returns
dynace_gen::Error::SignatureConflict { selector, existing_arity, discovered_arity }. - Preserves
sourcepaths. - Sorts and writes.
The scanner reads:
- Macro-emitted metadata statics (
__DYNACE_CLASS_META_*and__DYNACE_METHOD_META_*). class!andmethod!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.
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 (AtPut → AT_PUT, HTMLDoc → HTML_DOC).
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 FooClass
→ FooMetaClass: 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;andpub struct ClassNameMetaClass;(or the user's explicit metaclass marker name).impl DynaceClassMarker for ClassName {}andimpl 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.
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.
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.
M1(&mut world, obj, &[])The runtime:
class = class_of(obj).- Walks
CPL(class). - Searches each class for
M1in its method table. - Invokes the first match.
New(&mut world, DogClass::class_object(&world)?, &[])The runtime:
class = class_of(DogClassObject)— which isDogMetaClass.- Walks
CPL(DogMetaClass). - Searches metaclasses for
New. - Invokes the first match.
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 is read-only and deterministic. All methods are on World and
prefixed reflect_.
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>
reflect_supers(class) -> Result<Vec<ObjectRef>>reflect_cpl(class) -> Result<Vec<ObjectRef>>
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.
reflect_local_methods(class) -> Result<Vec<MethodDef>>— sorted by selector. Does not follow inheritance; usecall_genericorlookup_method_deffor inheritance-aware lookup.
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>
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)).
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.
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.
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.
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
Integration tests live in crates/dynace/tests/:
tests/object_model.rs— bootstrap roots, class-of spine, object kinds.tests/class_creation.rs—define_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-endclass!/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).
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.
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 VERSIONetc.dynace.css— minimal stylesheet for the HTML output.Makefile— targetshtml,pdf,all,clean. Outputs land indocs/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.
The system is acceptable when:
- Class names in normal code are Rust identifiers, not strings.
- Classes, metaclasses, and the metametaclass are ordinary objects.
- Ordinary instances can access their class object.
- Class variables are declared on metaclasses, stored on the declaring class, and shared by every descendant of that class.
- Instance variables are declared on classes and stored on instances.
- Instance variables inherit through the class CPL.
- Class variables inherit through the metaclass CPL.
- The metaclass hierarchy parallels the class hierarchy.
- Multiple inheritance works under C3 linearization.
- Methods are private to their defining class by default.
- Generic names equal their dispatched method names.
- Generics are generated, not hand-written.
- The generated generic is the public interface.
- The generated generic dispatches on the receiver's runtime class.
- Reflection exposes classes, metaclasses, variables, methods, and generics.
- All behaviour is covered by tests.
All sixteen criteria are met as of this writing (95 tests passing, 0 warnings).
- 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.
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.
-
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.
-
Explicit
override var namesyntax. Silent shadowing of an inherited instance or class variable is rejected withError::InconsistentInheritance. There is no syntax yet for explicit shadowing, which the original spec sketched asoverride var name. -
Explicit
sharedslot marker. Diamond inheritance (same logical variable reached via two CPL paths) collapses to one slot automatically. Two distinct ancestor classes contributing the same name producesError::AmbiguousSlot. The originally sketched "explicitly marked as shared" path for the latter case is not implemented. -
inventory/linkmescanner input. The scanner reads macro-emitted metadata statics andclass!/method!macro invocations. An alternate scanner input via theinventoryorlinkmecrates is not wired up; macro-invocation scanning provides the same end-to-end capability. -
Richer
MethodSignature. Method signatures currently trackarityonly. The runtime and the generator both use arity-based conflict detection. Tracking parameter and return types inMethodSignatureis a future extension that the generator and runtime would consume. -
Richer
TypeDescriptor. The currentTypeDescriptoris 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.