Blog › ICP guides
Smalltalk developer on retainer: binary message precedence, subclasses vs allSubclasses, Pharo enterprise systems, and Smalltalk on monthly retainer
October 2, 2026 · ~15 min read
A Smalltalk developer was maintaining an invoice calculation engine in Pharo for an insurance company. The engine had been in production for three years. The developer was adding a late fee calculation: when a client’s retainer payment arrived after the due date, a late fee equal to the unpaid amount multiplied by a penalty rate was added to the invoice total. The original calculation was:
invoiceTotal := baseAmount + flatFee.
After adding the late fee logic, the developer modified it to:
invoiceTotal := baseAmount + lateFee * penaltyRate.
The intended calculation was baseAmount + (lateFee * penaltyRate): add the scaled late fee to the base amount. The actual Smalltalk evaluation was (baseAmount + lateFee) * penaltyRate: add the base amount and the late fee first, then multiply the entire sum by the penalty rate. In Smalltalk, all binary messages have equal precedence and evaluate strictly left-to-right. The operators + and * are not built-in arithmetic operators with mathematical precedence rules — they are binary message sends, and in Smalltalk all binary messages have the same precedence level. 3 + 4 * 5 in Smalltalk evaluates to 35, not 23: 3 + 4 evaluates to 7, then 7 * 5 evaluates to 35. For a representative invoice with baseAmount = 850.00, lateFee = 75.00, and penaltyRate = 1.05, the intended result was 850.00 + (75.00 * 1.05) = 928.75. The actual Smalltalk result was (850.00 + 75.00) * 1.05 = 971.25. The twelve invoices generated for the month all had inflated totals. Wrong invoice calculations: 12 → 0. The fix was adding parentheses: invoiceTotal := baseAmount + (lateFee * penaltyRate). The work log said “fixed late fee calculation, 5h.” What is invisible is that Smalltalk has no mathematical operator precedence — this is a deliberate design decision, not an oversight — and that every developer who comes from C, Java, Python, Ruby, or any other language that inherits C’s operator precedence rules carries the assumption that * binds more tightly than +, an assumption that does not hold in Smalltalk.
Smalltalk overview: message-passing purity from Xerox PARC to Pharo
Smalltalk was developed at Xerox PARC between 1972 and 1980 by Alan Kay, Dan Ingalls, Adele Goldberg, and their colleagues. Smalltalk-80 was the first version released to the public (1980), and its design became the reference point for object-oriented programming as a paradigm. The language was built on three principles: everything is an object (including integers, classes, and methods themselves); objects communicate exclusively by message passing; and class definitions determine object behavior through method lookup in the class hierarchy. These principles were not just language design choices — they were a theory of computation intended to show that a complete, uniform object model could eliminate the special-case distinctions between data types, operators, and control structures found in languages like C and Pascal.
Modern Smalltalk implementations diverge significantly in class library, tooling, and deployment model. Pharo (open source; versions 10, 11, 12 are current) is the most actively developed implementation and is used in universities, data analytics, and web applications. It uses a Spur image format and the OpenSmalltalk VM; packages are managed via Metacello and loaded from GitHub. VisualWorks (commercial; by Cincom; version 9.x) is the enterprise implementation used in banking, insurance, and telecommunications — applications that have run in production on VisualWorks for 20 to 30 years. VA Smalltalk (by Instantiations; the successor to IBM’s VisualAge Smalltalk) is used in large enterprise installations, particularly in financial services. GNU Smalltalk is a file-based implementation that does not use the image model and is used for scripting and education. Dolphin Smalltalk targets Windows development. The retainer work associated with each implementation differs: Pharo retainers focus on Metacello package management and Pharo version migration; VisualWorks and VA Smalltalk retainers focus on image management, porting to new versions, and maintaining applications that cannot easily be rewritten.
The Smalltalk image is the primary artifact of a Smalltalk system. Unlike file-based languages where the source files are the authoritative record and the runtime is reconstructed from them at each startup, a Smalltalk image is a serialized snapshot of the entire runtime environment: all class definitions, all objects, all running processes, all open tool windows. The image is the deployment unit. A VisualWorks production system runs from a specific .im (image) file; updating the production system means modifying the running image and saving it, or loading change sets into the image from source files. This image-centric model means that “deploying a change” in VisualWorks is categorically different from deploying a change in a file-based language: it requires loading the change into the image, verifying the image starts cleanly, and saving a new image that becomes the deployment artifact.
Smalltalk retainer work today covers: VisualWorks and VA Smalltalk enterprise application maintenance (these systems are in production at financial institutions, government agencies, and telecommunications companies; the codebase is often 10 to 30 years old and maintained by a small team on monthly retainer); Pharo application development and maintenance (Pharo is used for new development in European universities, data analytics startups, and embedded IoT systems; the Pharo version upgrade cycle is rapid and retainer work includes migrating Metacello baselines and updating deprecated API calls); and binary precedence audits (any time a Smalltalk financial calculation is modified, the risk of a binary precedence error is elevated; retainer work includes auditing modified expressions for missing parentheses and writing property-based tests that verify arithmetic results against known expected values).
Binary message precedence: Smalltalk’s deliberate departure from mathematical convention
Smalltalk has exactly three levels of message precedence, applied in order from highest to lowest: unary messages (no arguments, like factorial, negated, size), binary messages (one argument, syntactically using non-alphanumeric characters like +, -, *, /, =, <, >), and keyword messages (one or more arguments named with trailing colons, like at:put:, inject:into:, ifTrue:ifFalse:). Within each precedence level, messages evaluate left-to-right. Parentheses override precedence at any level.
The implication: all binary messages — arithmetic operators, comparison operators, and any other binary message — have identical precedence. 2 + 3 * 4 evaluates as (2 + 3) * 4 = 20, not as 2 + (3 * 4) = 14. 100 - 20 / 4 evaluates as (100 - 20) / 4 = 20, not as 100 - (20 / 4) = 95. a + b < c * d evaluates as ((a + b) < c) * d, which sends the * message to a Boolean result, raising a doesNotUnderstand: #* error (Booleans do not respond to *). Correct form: (a + b) < (c * d). This is not a bug in Smalltalk — it is the specification. Alan Kay’s rationale: arithmetic operators are binary messages, not special syntax; special-casing their precedence would introduce exactly the kind of irregularity the language design aims to eliminate. The cost is that every developer coming from another language must unlearn the operator precedence they internalized and substitute a purely left-to-right model.
The diagnostic for a binary precedence bug: evaluate the expression in the Pharo Playground or VisualWorks Transcript with representative values, printing the result with Transcript show: expression printString. If the result is wrong, add parentheses one level at a time to identify which sub-expression is evaluating in the wrong order. The systematic approach: trace the expression as a sequence of binary message sends, applying each from left to right, and verify at each step that the intermediate result is the intended intermediate result. For complex financial expressions, the correct approach is to break the expression into named temporaries:
| scaledLateFee |
scaledLateFee := lateFee * penaltyRate.
invoiceTotal := baseAmount + scaledLateFee.
Named temporaries make the evaluation order explicit and auditable. They also make the code self-documenting at the sub-expression level: scaledLateFee names the semantics of lateFee * penaltyRate explicitly.
Class hierarchy traversal, collection protocol, and the Smalltalk image
Smalltalk class queries require understanding the distinction between direct and transitive relationships. aClass subclasses returns an Array of the classes that directly inherit from aClass — the classes whose superclass is aClass. aClass allSubclasses returns an OrderedCollection of all classes that inherit from aClass at any depth. aClass allSuperclasses returns all ancestors of aClass up to ProtoObject (in Pharo; Smalltalk-80 uses Object as the root). The inverse queries: anObject class returns the class of the object; anObject isKindOf: aClass returns true if the object is an instance of aClass or any of its subclasses (uses allSuperclasses traversal); anObject isMemberOf: aClass returns true only if the object is a direct instance of exactly aClass.
The distinction between isKindOf: and isMemberOf: matters for type dispatch: result isKindOf: Number returns true for integers, floats, fractions, and large integers (all subclasses of Number); result isMemberOf: Float returns true only for IEEE double-precision floats, not for SmallFloat (boxed floats in Pharo) or LargeFloat. In Smalltalk, the idiomatic type check is isKindOf: for capability testing, because Smalltalk’s polymorphism model assumes that subclasses respond to the same interface as their superclass; an integer is a number in the intended semantic sense. isMemberOf: is for the rare cases where the exact concrete class matters (e.g., when the object will be serialized by a mechanism that requires knowing the precise class).
Smalltalk’s collection protocol is rich and uniform: every collection responds to do: (iterate with one-argument block), collect: (transform, returns new collection of same kind), select: (filter, keep elements where block returns true), reject: (filter, keep elements where block returns false), detect: (find first element where block returns true; raises error if none), detect:ifNone: (find first element or evaluate default block), inject:into: (fold/reduce; two-argument block receives accumulator and element). A common retainer bug: using detect: without ifNone: in code that may not find a matching element — the unhandled Error: Element not found fires at runtime. Fix: use detect:ifNone: with an appropriate default. Another common bug: using collect: on a Dictionary when associations collect: is intended (Dictionary’s collect: passes values only, not key-value pairs).
Pharo package management uses Metacello and Baseline classes. A BaselineOf subclass defines the package dependency graph for a project; Metacello loads it by evaluating Metacello new baseline: 'ProjectName'; repository: 'github://user/repo:branch'; load. Retainer issues: a Baseline that was written for Pharo 10 fails to load in Pharo 12 because an API it depends on was deprecated and removed; a Baseline that specifies a branch name that no longer exists in the remote repository; a Baseline that loads packages in the wrong order, causing class definition failures when a package tries to subclass a class not yet loaded. Diagnosing these requires reading the Metacello load log, identifying the failing package and the specific API that is missing, finding the Pharo 12 replacement API, and updating the Baseline or the affected package.
Typical Smalltalk retainer work and what it looks like in a work log
Binary message precedence diagnosis is the largest category of Smalltalk retainer work that produces no visible artifact. Financial calculation expressions are the highest-risk location: any expression that mixes +, -, *, and / without parentheses is a potential precedence bug, because the developer writing the expression in a context where they think about the arithmetic semantics will intuitively apply mathematical operator precedence rules — rules that do not exist in Smalltalk. The bug is most likely to appear when code is modified: the original expression was parenthesized, the modification adds a new term without parentheses, and the modified expression evaluates differently from what the developer intended. Work log entry: “Invoice calculation: baseAmount + lateFee * penaltyRate; Smalltalk evaluated as (baseAmount + lateFee) * penaltyRate = (850.00 + 75.00) * 1.05 = 971.25; intended evaluation: baseAmount + (lateFee * penaltyRate) = 850.00 + 78.75 = 928.75; wrong invoices for the month: 12; fix: added parentheses baseAmount + (lateFee * penaltyRate); verified against 36 test cases covering zero, positive, and large late fees; wrong calculations before: 12; after: 0; 5h.”
Image migration is the second category. A VisualWorks 7.x image being upgraded to VisualWorks 9.x must be tested method by method against the API changes documented in the version release notes. Common breakages: a class that was renamed (the old name still exists as a deprecated alias in some versions, not in others); a method that was removed rather than deprecated (calls raise doesNotUnderstand: at runtime, not a load-time error); a method whose return type changed from one collection class to another (code that called collect: on the result of a query that used to return an OrderedCollection now returns an Array; both respond to collect:, so the code runs but the downstream code that called add: on the result raises doesNotUnderstand: #add: because Array is fixed-size). Work log entry: “VisualWorks 7.x to 9.x migration: PolicyRecord allPolicies returned OrderedCollection in 7.x, returns Array in 9.x; downstream code called result add: newRecord; doesNotUnderstand: #add: raised in report generator; fix: wrapped in OrderedCollection withAll: PolicyRecord allPolicies at the call site; migration failures before: 1 per report run; after: 0; 8h.”
Collection protocol selection is the third category. An OrderedCollection accumulates all elements added to it, including duplicates. A Set eliminates duplicates based on equality (=) and identity (hash). Using OrderedCollection where Set semantics are needed (unique-element collection) produces a collection that grows indefinitely with repeated additions of the same element. Using Set where ordering is needed produces a collection whose iteration order is undefined (hash-based, not insertion-order-based). Work log entry: “Tag collection: TagRegistry used OrderedCollection for unique-tag list; addTag: called multiple times with the same tag; duplicates accumulated over session lifetime; report generator counted 47 tags where 8 unique tags existed; fix: changed TagRegistry to use Set internally; size before fix: 47 for 8 unique tags; after: 8; 3h.”
Track Smalltalk developer retainer hours without the status emails
When a 5-hour session traces 12 wrong invoice totals to a missing parenthesis in baseAmount + lateFee * penaltyRate — because Smalltalk evaluates all binary messages left-to-right with no mathematical operator precedence, multiplying the entire sum by the penalty rate instead of only the late fee — the work log needs to name the expression, the Smalltalk evaluation order, the intended vs actual result, and the wrong-calculation count before and after. HourTab gives your Smalltalk retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the message precedence mechanism. No client login. No status emails. CSV in, URL out.
How HourTab tracks Smalltalk developer retainer hours
Smalltalk retainer work is invisible by the same mechanism that makes Smalltalk’s message-passing model elegant: arithmetic operators are ordinary binary messages, indistinguishable from any other binary message in the language grammar. A Smalltalk compiler does not know that * is “multiplication” and + is “addition” in the mathematical sense — it only knows that they are binary messages and evaluates them left-to-right. The result of baseAmount + lateFee * penaltyRate is a valid Smalltalk expression that compiles and runs without warning. The result is wrong by the exact amount that the base amount amplification differs from the late fee amplification — a difference that varies per invoice, making the bug difficult to detect by spot-checking. The twelve wrong invoices sent to clients had plausible-looking totals; none triggered an immediate complaint.
The work log needs to name the mechanism: which expression, what Smalltalk evaluated it as (left-to-right without mathematical precedence), what the intended evaluation order was (with the multiplication grouped first by parentheses), and the concrete before-and-after wrong-calculation count. A log entry that says “fixed invoice calculation, 5h” is not auditable. A log entry that says “Invoice late fee: baseAmount + lateFee * penaltyRate; Smalltalk binary message precedence: all binary messages equal, left-to-right; evaluated as (850.00 + 75.00) * 1.05 = 971.25; intended: 850.00 + (75.00 * 1.05) = 928.75; wrong totals by baseAmount * (penaltyRate - 1.0) = overcharge; wrong invoices before: 12; fix: parentheses baseAmount + (lateFee * penaltyRate); verified on 36 test cases; after: 0; 5h” is auditable.
HourTab gives Smalltalk developers a public retainer-hours URL they send to clients — insurance companies and financial institutions running VisualWorks or VA Smalltalk production systems, universities and research organizations using Pharo for data analytics or simulation, and technology companies with legacy Smalltalk codebases that predate the modern software ecosystem. For Smalltalk retainers, each work log entry should name the Smalltalk-specific mechanism: binary message precedence, class hierarchy depth, collection protocol semantics, or image migration API change. Comparative context: Smalltalk retainer work has conceptual overlap with other languages that have simplified operator precedence — APL and J (which have no operator precedence, evaluating right-to-left uniformly), and Forth (which has no operators at all, only words on a stack) — but in mainstream languages Smalltalk is unique in treating arithmetic operators as ordinary binary messages with uniform precedence. Every developer who has written Python, JavaScript, Java, C, or Ruby brings an operator precedence model that does not transfer to Smalltalk; for long-running VisualWorks systems where the original developers have left, binary precedence bugs introduced by maintenance programmers from other language backgrounds are a recurring retainer category.
FAQ: Smalltalk developer retainers
What does a Smalltalk developer on retainer typically do?
A Smalltalk developer on monthly retainer covers binary message precedence diagnosis (+ and * have identical precedence and evaluate left-to-right; a + b * c evaluates as (a + b) * c in Smalltalk, not a + (b * c)); class hierarchy traversal repair (subclasses vs allSubclasses; isKindOf: vs isMemberOf:); Pharo and VisualWorks application maintenance (class library API changes between versions, Metacello Baseline management, image migration); block closure and exception handling debugging (on:do:, ensure:, detect:ifNone:); and image management for deployed VisualWorks and VA Smalltalk systems.
What Smalltalk work is most commonly underlogged?
Binary message precedence diagnosis is the most underlogged Smalltalk retainer work: tracing a wrong arithmetic result to an expression like baseAmount + lateFee * penaltyRate where the developer intended baseAmount + (lateFee * penaltyRate) but Smalltalk evaluated it as (baseAmount + lateFee) * penaltyRate; the fix is parentheses; 3 to 7 hours of diagnosis produces a parenthesis insertion. Class hierarchy traversal: subclasses vs allSubclasses; plugin discovery systems returning only direct subclasses; 2 to 5 hours invisible. Image migration: VisualWorks or VA Smalltalk API changes between versions surfacing as doesNotUnderstand: at runtime; 5 to 12 hours invisible. Collection protocol: detect: without ifNone: on collections that may be empty; OrderedCollection vs Set semantics; 3 to 6 hours invisible.
What are typical Smalltalk developer retainer rates?
Entry-level Smalltalk developers with 1 to 2 years covering basic class and method definitions, collection protocol, block evaluation, and Pharo tooling typically bill at $65 to $120 per hour. Mid-level Smalltalk programmers with 2 to 4 years covering class hierarchy traversal, metaclass method dispatch, exception handling, Pharo package management, and image migration typically bill at $95 to $175 per hour. Senior Smalltalk developers with 4 or more years covering VisualWorks or VA Smalltalk enterprise application maintenance, full metaclass hierarchy debugging, Seaside or Teapot web maintenance, and legacy image migration typically bill at $140 to $255 per hour. Monthly retainer ranges: $1,500 to $3,000 per month for advisory engagements (12 to 20 hours per month); $3,000 to $9,000 per month for full engagement VisualWorks or Pharo application development or enterprise image maintenance.
What should a Smalltalk developer retainer agreement include?
A Smalltalk developer retainer agreement should specify: implementation scope (Pharo, VisualWorks, VA Smalltalk, GNU Smalltalk — each has different class libraries, image formats, and tooling; the implementation determines the applicable APIs); image management scope (who maintains the canonical image file, whether the engagement covers image migration across major versions, whether Metacello package management is in scope); binary precedence audit scope (whether the engagement includes an audit of arithmetic expressions in financial calculation code for missing parentheses); Pharo version scope if Pharo (Pharo 11 vs 12 have API differences); and hour logging format (the expression that produced the wrong result, the Smalltalk evaluation order actually applied, the intended vs actual result, the fix applied, and the wrong-calculation count before and after).
How should Smalltalk developer retainer hours be logged?
Log each Smalltalk retainer session with: the expression that produced the wrong result (e.g., invoiceTotal := baseAmount + lateFee * penaltyRate); the Smalltalk evaluation order applied (e.g., all binary messages equal precedence, left-to-right: (850.00 + 75.00) * 1.05 = 971.25); the intended evaluation order (e.g., 850.00 + (75.00 * 1.05) = 928.75); the wrong-result count (e.g., 12 invoices for the month); fix applied (e.g., added parentheses: baseAmount + (lateFee * penaltyRate)); wrong invoice calculations before: 12; after: 0. For class hierarchy bugs: the query used, expected vs actual result, fix, wrong-discovery count. For image migration bugs: the API removed or changed, the error message, the fix, and startup failure count before and after.