Blog › ICP guides
Self developer on retainer: prototype-based OOP, clone semantics, parent slot sharing, slot types, and Self language on monthly retainer
October 1, 2026 · ~15 min read
A Self developer was building a mixin library for a prototype-based object hierarchy. The design used a shared base prototype that several specialized prototypes were cloned from. The developer cloned the base prototype to create a specialized variant, then added a new method to the specialized variant’s parent slot to provide behavior that the variant alone should have. When the developer tested the system, the new method appeared not just on the specialized variant but also on the original base prototype and on all other clones derived from it. The developer had expected that cloning created isolation: a clone was a new object, and changes to the clone should not affect the original. Self’s clone operation does copy all slots — but it copies parent slots by value, meaning it copies the slot contents (an object reference) by value. Both the original and the clone have a parent slot whose value is the same object reference. When the developer added a method to what they thought was the clone’s parent, they were adding a method to the shared parent object that both the original and the clone delegated to. Method isolation cases: 1 → 0 after deep-copying the parent slot value to create an independent parent for the specialized clone.
The work log said “fixed method appearing on wrong prototype, 6h.” It cannot explain the mechanism: that Self’s clone operation copies parent slots by reference to their values rather than by deep-copying the objects those slots hold, that both the original and the clone were delegating to the same shared object, or that the fix required understanding the difference between copying a slot (which clone does) and copying the object a slot points to (which clone does not do). A client reading that entry sees six hours for what reads like a naming bug or a method-placement mistake. What is invisible is the diagnostic cost: confirming that the method was added to the correct slot in the clone’s view of the object graph, determining that the parent slot value was a shared reference, tracing the delegation chain to identify all objects affected by the shared parent, and deciding whether the correct fix was to deep-copy the parent, to add the method directly to the clone’s own slots instead of to the parent, or to restructure the prototype hierarchy so that the shared parent only contains behavior intended to be universal.
Self language overview: the original prototype-based object-oriented language
Self was created at Sun Microsystems Research by David Ungar and Randall Smith, with the first public description in a 1987 OOPSLA paper: “Self: The Power of Simplicity.” Self’s central claim was that class-based OOP had the wrong fundamental abstraction: classes are descriptions of objects, but developers reason about individual objects. Self removed classes entirely and made every object a prototype — a concrete, inspectable, modifiable object that can be cloned to produce new objects. The language found its most enduring influence not in direct adoption but in subsequent systems that adapted its ideas: JavaScript’s prototype-based inheritance was directly inspired by Self, and the Self VM’s type-feedback JIT compilation techniques were the foundation for the HotSpot JVM’s JIT compiler, making Self indirectly one of the most influential systems in the history of runtime optimization.
Self code consists entirely of message sends. There are no keywords, no class declarations, no method bodies outside of objects, no type annotations. Every computation is expressed as sending a message to a receiver: 3 + 4 is sending the message + with argument 4 to the receiver 3. Blocks (closures) are objects that respond to the message value or value: arg. Control flow is entirely message-based: conditionals are booleanExpr ifTrue: [trueBlock] ifFalse: [falseBlock]. Loops are [condition] whileTrue: [body] or collection do: [: each | body]. Assignment sends the message name: (the name followed by a colon) to the object that holds the slot: self name: 'Alice' assigns the value 'Alice' to the slot name in the current object. Primitive operations are expressed as message sends to a primitive: <primitive: 'add:with:'> inside a method invokes the VM-level addition primitive.
The Self environment is image-based: the entire state of the system — all objects, all prototype definitions, all code — lives in a persistent image file. A developer session begins by loading the image, making changes (adding slots, modifying method bodies, cloning prototypes), and ends by saving the image. There is no separate compilation step; the development cycle is an interactive dialogue with a live object graph. The Self development environment is graphical: objects are displayed as rectangles with their slots visible, and developers interact with the object graph by pointing and clicking. This image-based model means that there is no source-file representation of a Self program separate from the image; the image is the source of truth.
Self’s slot types: data slots, method slots, and parent slots
A Self object is a collection of named slots. Each slot has a name and a value. The behavior of a slot is determined by its type:
Data slots hold values. A data slot named x with value 42 responds to the message x by returning 42. Mutable data slots also respond to the setter message x: with a new value, updating the slot. Constant data slots (annotated in the environment) cannot be updated after initialization. Data slots hold any Self object: integers, strings, blocks, other prototypes.
Method slots hold code objects (methods). A method slot named area responds to the message area by executing the code in the method. Method slot bodies have access to self (the receiver of the message) and to the slot values of self via message sends. Methods are full first-class objects in Self: a method slot’s value is a code object that can be inspected, modified, and sent as a value to other objects.
Parent slots are the mechanism for delegation. A parent slot is annotated with an asterisk in the Self environment. When a message is sent to an object and the object does not have a slot with that name, Self follows all parent slots in the object to look for the slot in the parent objects. This delegation is recursive: if the parent object also does not have the slot, Self follows the parent’s parent slots. A chain of parent pointers forms the delegation chain. Multiple parent slots create multiple inheritance: if an object has two parent slots pointing to two different objects, a message that neither the object nor either parent has will cause a method-not-found error, and a message that both parents have (same slot name in two parents) causes an ambiguity error.
The critical property of parent slots for clone semantics is that cloning copies the parent slot’s value by reference. The parent slot is a data slot whose value happens to be another prototype object. When you clone an object, the clone gets a parent slot with the same value as the original — the same object reference. Both the original and the clone now have parent slots pointing to the same shared parent object. If you subsequently send _AddSlots: to that shared parent object (to add a new method slot to it), the method appears in the delegation chain of both the original and the clone, because both delegate to the same parent.
Clone semantics: what clone copies and what it shares
The clone operation in Self creates a shallow copy. Every slot in the original object is copied into the new clone object: data slots are copied with their current values, method slots are copied with their code objects, parent slots are copied with their object references. The distinction between “shallow” and “deep” is in what “copy with their current values” means for each slot type:
For data slots holding primitive values (integers, strings), the copy is effectively a deep copy: integers and strings are immutable, so the clone having its own slot with the value 42 is indistinguishable from having a shared reference to 42. For data slots holding mutable object references (including method code objects and parent prototypes), the copy is a shallow copy: the clone’s slot contains a reference to the same object as the original’s slot. Assigning a new value to the clone’s slot via a setter message — clone parent*: newParentObject — replaces the slot value in the clone without affecting the original. But modifying the object that the slot points to — for example, adding a new slot to the shared parent via sharedParent _AddSlots: (| newMethod = [...] |) — changes the shared parent itself and therefore affects all objects whose parent slot points to it.
This distinction — between assigning a new value to the clone’s slot (affects only the clone) and modifying the object the slot points to (affects all objects sharing that reference) — is the source of the most common confusion in Self retainer work. A developer who wants to add behavior to a clone and only to that clone must either: (1) add a method slot directly to the clone itself, not to its parent; (2) deep-copy the parent slot value before adding to it, so the clone has its own independent parent object; or (3) restructure the prototype hierarchy so that the method is added to a new intermediate prototype that only the specialized clone delegates to.
To deep-copy the parent slot value, the developer sends parent copy to the shared parent object to create a new independent copy, then replaces the clone’s parent slot with this new copy: clone parent*: parent copy. After this assignment, the clone has a parent slot pointing to a new, independent object. Adding a method to this new parent via clone parent _AddSlots: (| newMethod = [...] |) affects only the clone’s delegation chain, not the original’s. The original’s parent slot still points to the original shared parent.
Self’s message send syntax and the prototype dispatch chain
Self’s message syntax is derived from Smalltalk but simplified. Unary messages are single identifiers: object clone sends the clone message. Binary messages are operators: x + y, a = b. Keyword messages end in colons and take one argument per keyword: object at: index, object at: index Put: value. Multi-keyword messages are sent as a single message with all keywords concatenated: at:Put: is one message name, not two.
The dispatch algorithm is: send message m to receiver r. (1) Look for a slot named m in r. If found and it is a method slot, execute it with self bound to r. If found and it is a data slot, return its value. (2) If not found in r, collect all parent slots of r (slots annotated with *). Recursively look for slot m in each parent. (3) If found in exactly one parent chain, use that resolution. (4) If found in multiple parent chains (ambiguity), raise an error. (5) If not found in any parent chain, invoke the doesNotUnderstand: message protocol.
The prototype dispatch chain is thus the analog of a class hierarchy in Self’s model. A typical Self object has one parent slot pointing to a prototype that provides general methods; that prototype has a parent slot pointing to a more general prototype; and so on up to the root prototype lobby. Multiple parent slots at any level create multiple inheritance. The standard Self object library uses a traits pattern: a prototype object anObject has a parent slot pointing to traits anObject, which is a separate prototype holding all methods. This separation means that cloning anObject creates a new instance with its own data slots but sharing the method prototype traits anObject through the parent slot. Since both the original and clone share traits anObject, methods added to traits anObject appear on both — which is the intended behavior for methods that all instances should have. This is the traits design pattern in Self: it uses the same mechanism (shared parent) but makes the sharing intentional and architecturally explicit.
The Self VM and type-feedback JIT compilation
The Self VM was the first practical implementation of type-feedback JIT compilation. The technique begins with an observation: in Self’s dynamic dispatch model, the type of a message receiver is not known until runtime — there are no type declarations in Self source code. A naive implementation would perform a full slot lookup on every message send, which would be slow. The Self VM’s solution was to observe, at runtime, what type of object was actually the receiver of each message send, and speculate that it would be the same type on subsequent calls.
The type feedback mechanism records the concrete type of the receiver at each send site. When a particular send site has always had the same receiver type, the JIT compiles a type-checking guard followed by a direct call to the method resolved for that type, bypassing the lookup entirely. If the guard fails (a different type appears), the VM falls back to the general case and updates its type profile. For the common case where the receiver type is stable — which describes most message sends in real Self programs — the compiled code is nearly as fast as a direct function call. The guards add a small overhead that is consistently faster than full dynamic dispatch.
This technique, developed for Self in the early 1990s by David Ungar, Craig Chambers, Bay-Wei Chang, and Urs Hölzle, was subsequently adapted for the HotSpot JVM. HotSpot’s tiered JIT compilation uses the same core idea: observe types at runtime, speculate on stability, generate optimized code with guards, and deoptimize when speculation fails. The lineage from Self to HotSpot makes Self a system of historical significance even for developers who will never write Self code directly: understanding Self’s object model and the constraints it imposed on the JIT design illuminates why HotSpot is architected the way it is. A retainer engagement with Self expertise therefore often includes consulting on dynamic language performance analysis more broadly, which is a context where the rates reflect both the direct Self knowledge and the broader runtime systems expertise.
Typical Self retainer work and what it looks like in a work log
A Self retainer typically covers three recurring categories of work. The first is prototype hierarchy design and clone semantics auditing: identifying which parent slots in the hierarchy are shared and whether that sharing is intentional, redesigning hierarchies to separate shared behavior (in traits prototypes, shared by design) from per-instance behavior (in own data and method slots, not shared), and documenting the delegation chain for each major prototype. This work produces no visible feature — it produces a hierarchy that behaves correctly under clone operations. The work log entry “audited parent slot sharing in mixin library, isolated 3 unintentional shares, 6h” is auditable: the three deep-copy operations are visible in the image as new independent parent objects. Without explaining what “unintentional share” means in Self’s model — two objects delegating to the same parent when the developer expected them to have independent parents — the entry is not self-explaining to a client unfamiliar with prototype semantics.
The second category is dispatch chain analysis and conflict resolution. A Self prototype hierarchy with multiple parent slots creates the potential for dispatch ambiguity: two parents may define the same slot name. Resolving these conflicts requires tracing the delegation chain, identifying which parent’s resolution is semantically correct for each conflicting slot name, and restructuring the hierarchy to eliminate the ambiguity — either by removing the conflicting slot from one parent or by adding an own slot to the child prototype that overrides the parent dispatch. This diagnostic work is entirely invisible in a work log that says “resolved dispatch ambiguity, 4h.”
The third category is Self VM image management. The image-based development model means that the image file is the authoritative state of the system. Retainer work includes establishing image save conventions (when to save, how to name images, how to verify a save was complete), diagnosing image issues when a save was interrupted or a load produces unexpected state, and managing image size as the object graph grows over the course of a project. The work log entry reads “cleaned up image after interrupted save, recovered 4 lost method edits from backup, 3h” and the hours are justified by the recovery work and the procedures established to prevent future interrupted saves from losing work.
Track Self developer retainer hours without the status emails
When a six-hour session traces a method appearing on the wrong prototype back to a shared parent slot reference from a shallow clone operation, deep-copies the parent to isolate the specialized clone, and verifies that the delegation chain is correct for all affected prototypes, the work log needs to say that — not just “fixed method on wrong object.” HourTab gives your Self retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the shared parent, the deep-copy operation that isolated it, and the method isolation cases before and after. No client login. No status emails. CSV in, URL out.
See HourTab pricing →How HourTab tracks Self developer retainer hours
Self retainer work is invisible by the same mechanism that makes prototype-based OOP powerful: behavior emerges from the object graph, not from class declarations, so changes to the object graph — adding slots, restructuring parent chains, deep-copying parent slot values — are small operations with large behavioral implications. A client who sees “6h — fixed prototype hierarchy” cannot assess whether six hours was proportionate to what appears to be a few slot additions. The work log needs to say: the clone operation produced a specialized prototype whose parent slot pointed to the same object as the original’s parent slot; adding a method to what was believed to be the specialized prototype’s parent in fact added it to a shared object that both original and clone delegated to; the shared parent was deep-copied with clone parent*: parent copy to create an independent parent for the specialized clone; the new method was added to the now-isolated parent; the method now appears only on the specialized clone, not on the original or other clones; isolation cases: 1 → 0. That log entry is auditable and justifies the hours by showing the mechanism of the error and the mechanism of the fix.
HourTab gives Self developers a public retainer-hours URL they send to clients — typically research groups working with Self’s object model for prototype-based system design, developers maintaining Self codebases for educational or historical purposes, teams using Self’s descendants (JavaScript, Lua) who want to understand the semantics of prototype-based dispatch at a foundational level, and runtime systems researchers studying Self’s type-feedback JIT techniques. For Self retainers, each work log entry should name the mechanism at the level of the object model: which parent slot was shared, what made it shared (shallow clone), how it was isolated (deep copy or own-slot addition), and what the before and after count of method-on-wrong-prototype incidents is. Comparative context for scope discussions: Self retainer work has conceptual overlap with retainer work on JavaScript (which inherited Self’s prototype model), Lua (prototype-based metatables), and Io language (direct Self descendant). Senior Self expertise commands $150 to $265 per hour because the combination of prototype-based OOP design depth, Self VM runtime knowledge, and historical systems understanding is extremely rare.
FAQ: Self developer retainers
What does a Self developer on retainer typically do?
A Self developer on monthly retainer covers Self’s prototype-based object model (no classes; objects are prototypes; clone creates a new object with copied slots; parent slots annotated with * provide delegation targets for message lookup; clone copies parent slot values by reference — both original and clone share the same parent object, so adding a slot to the shared parent affects both); slot types (data slots: read via message, written via setter; method slots: code executed when message is sent; parent slots: delegation targets for lookup; constant slots: immutable values); dispatch chain (lookup in object first, then recursively in all parent slots; ambiguity error if same name found via two parents; doesNotUnderstand: for missing slots); Self VM JIT (type feedback records receiver types at each send site; speculative compilation with type-checking guard; deoptimize on guard failure; ancestor of HotSpot JVM JIT); and image-based environment (entire object graph in persistent image file; save/load cycle; graphical development environment).
What Self work is most commonly underlogged in a retainer?
Clone semantics diagnosis and parent slot isolation (tracing a method appearing on the wrong prototype back to shared parent slot reference from a shallow clone; deciding between deep-copy, own-slot addition, or hierarchy restructuring; 5 to 8 hrs invisible per incident); prototype hierarchy design (separating shared traits from per-instance slots; designing delegation chains for multiple inheritance scenarios; 5 to 10 hrs invisible); dispatch chain debugging (identifying which parent in a delegation chain a message resolved to; resolving diamond inheritance slot-name conflicts; 4 to 7 hrs invisible); VM image management (save/restore procedures; image size optimization; recovery from interrupted saves; 3 to 6 hrs invisible); and mixin and trait pattern design (using multiple parent slots for trait composition; ensuring trait prototypes do not carry mutable state that would be shared via parent reference; 4 to 7 hrs invisible).
What are typical Self developer retainer rates?
Entry-level Self developers with 1 to 2 years of experience covering Self’s object model, clone and slot operations, message sending, and the Self environment typically bill at $60 to $110 per hour. Mid-level Self programmers with 2 to 4 years covering clone semantics diagnosis, parent slot isolation, prototype hierarchy design, dispatch chain analysis, and VM image management typically bill at $100 to $175 per hour. Senior Self language developers with 4 or more years covering advanced hierarchy design, type feedback JIT analysis, mixin and trait composition, image-based persistence architecture, and Self’s influence on JavaScript and HotSpot typically bill at $150 to $265 per hour. Monthly retainer ranges: $1,800 to $3,500 per month for advisory engagements (15 to 25 hours per month); $4,000 to $12,000 per month for full engagement Self development.
What should a Self developer retainer agreement include?
A Self developer retainer agreement should specify: clone semantics scope (parent slot sharing audit; unintentional share identification; deep-copy vs own-slot fix policy; method isolation incident rate); hierarchy design scope (traits prototype separation; multiple parent slot design; delegation chain documentation); dispatch chain scope (conflict identification; diamond inheritance resolution; disambiguation design); VM and image scope (Self VM version; image save and restore procedures; image size management; interrupted-save recovery); and hour logging format (clone: original, clone, parent slot, shared parent object, fix applied, isolation cases before and after; hierarchy: delegation chain before and after, conflicts resolved; dispatch: message, resolution path, conflict type; image: save trigger, objects affected, recovery steps).
How should Self developer retainer hours be logged?
Log each Self retainer session with: clone semantics category (original prototype; clone operation; parent slot name; shared parent object identity; method added to shared parent that appeared on wrong prototype; fix: clone parent*: parent copy to deep-copy, or method added to clone’s own slots; method isolation cases before: 1; after: 0; whether hierarchy restructuring was needed); hierarchy design category (prototypes involved; delegation chain before; delegation chain after; which methods moved from shared parent to per-instance slots; new prototype created for isolated behavior; dispatch chain length before and after); dispatch debugging category (message name; expected resolution prototype; actual resolution prototype; delegation chain path traced; conflict type: same slot in two parents; disambiguation applied; resolution after); VM image category (image file; save trigger; objects changed since last save; whether image was restored successfully; image size before and after); and for all categories, the before and after count of method-appearing-on-wrong-prototype incidents, since that count is the primary quality metric for Self retainer work on prototype hierarchy correctness.