- Introduction
- Tooling and Execution
- Lexical Structure
- Object System
- Multiple Inheritance
- Methods and Message Sending
- Blocks and Closures
- Control Flow
- Exception Handling
- Libraries and Namespaces
- Green Threads and Processes
- Primitives
- Smalltalk Compatibility
- JSON Support
- Grammar Reference
Harding is a class-based Smalltalk dialect that compiles to Nim. It preserves Smalltalk's message-passing semantics and syntax while making pragmatic changes for a modern implementation.
- Class-based object system with multiple inheritance
- Message-passing semantics (unary, binary, keyword)
- Block closures with lexical scoping and non-local returns
- Direct slot access for declared slots (O(1) access)
- Method definition syntax using
>>operator - Libraries for namespace isolation and modular code organization
- Green threads for cooperative multitasking
- Nim integration via primitive syntax
| Feature | Smalltalk-80 | Harding |
|---|---|---|
| Object model | Classes + metaclasses | Classes only (no metaclasses) |
| Statement separator | Period (.) only |
Period or newline |
| String quotes | Single quote (') |
Double quote (") only |
| Class methods | Metaclass methods | Methods on class object |
| Class variables | Shared class state | Not implemented |
| Instance variables | Named slots | Indexed slots (faster) |
| Multiple inheritance | Single only | Multiple with conflict detection |
| Primitives | VM-specific | Nim code embedding |
| nil | Primitive value | Instance of UndefinedObject class |
Harding includes multiple tools for different workflows:
harding- REPL and script executiongranite- compile Harding source to Nim/native binariesharding_debug- interpreter build with debugger server supportharding-lsp- Language Server Protocol supportbona- GTK-based IDE
Optional builds can enable the MummyX HTTP server bridge. See MUMMYX.md.
Typical workflows:
# Interactive REPL
harding
# Run a script
harding script.hrd
# Run a script with runtime arguments
harding script.hrd -- one two three
# Compile and run natively
granite run script.hrd --releaseFor full command options and debugging workflows, see TOOLS_AND_DEBUGGING.md.
42 # Integer
3.14 # Float
"hello" # String (double quotes only)
"""
hello
world
""" # Triple-quoted multiline string
#symbol # Symbol
#"""
multi
line
""" # Triple-quoted multiline symbol
#(1, 2, 3) # Array
#{"key" -> "value"} # Table (dictionary)
json{"x": 10} # JSON literal (returns JSON string)# This is a comment
#==== Section headerNote: Hash # followed by whitespace or special characters marks a comment. Double quotes are for strings, not comments.
#symbol # Simple symbol
#at:put: # Keyword symbol
#"with spaces" # Symbol with spaces (double quotes)
#"""
line 1
line 2 with "quotes"
""" # Multiline symbol stringHarding supports triple-quoted string literals using """...""".
text := """
Hello
He said "welcome"
Literal #{name}
Backslash-n: \n
"""Triple-quoted strings behave as follows:
- They are raw strings. Backslashes are preserved literally, so
\nstays two characters. - A single
"inside the string does not need escaping. - If the opening delimiter is followed immediately by a newline, that first newline is removed.
- Common indentation is stripped from all non-empty lines, making indented source blocks easier to write.
- The trailing newline before the closing delimiter is preserved.
This differs slightly from Nim: Harding matches Nim's raw triple-quoted handling for quotes and backslashes, but also strips leading indentation to make indented multiline literals easier to embed in Harding code.
Harding supports prefix JSON literals using json{...}:
json{"x": 10, "name": "Alice"}This returns a JSON string, not a Table. The prefix is case-insensitive:
json{"ok": true}
Json{"ok": true}
JSON{"ok": true}All three forms normalize to the Json class internally.
Shebang lines are supported for executable scripts. See Script Files for details.
Harding takes a pragmatic approach to statement separation:
# Periods work (Smalltalk-compatible)
x := 1.
y := 2.
# Line endings also work (Harding-style)
x := 1
y := 2
# Mixed style is fine
x := 1.
y := 2
z := x + y.Multiline keyword messages can span multiple lines while forming a single statement:
tags isNil
ifTrue: [ ^ "Object" ]
ifFalse: [ ^ tags first ]This is parsed as: tags isNil ifTrue: [...] ifFalse: [...] - a single statement.
| Construct | Multiline? | Example |
|---|---|---|
| Binary operators | No | x followed by newline then + y fails |
| Unary message chains | No | obj followed by newline then msg fails |
| Method selectors | No | Class>> followed by newline then selector fails |
| Keyword message chain | Yes | obj msg1: a\n msg2: b works |
| Statement separator | Yes | x := 1\ny := 2 works |
Harding scripts are stored in .hrd or .harding files and executed with:
harding script.hrdScripts are automatically wrapped in a block, enabling Smalltalk-style temporary variable declarations at the file level:
# script.hrd
| counter total |
counter := 0
total := 0
1 to: 5 do: [:i |
counter := counter + 1
total := total + i
]
total "Returns 15"This eliminates the need to use uppercase global variables (Counter, Total) for simple scripts.
Script blocks execute with self = nil, following the Smalltalk workspace convention (like a do-it in a Workspace). This provides consistent behavior between the REPL and script execution.
# In script.hrd or REPL do-it:
self printString "Returns: 'an UndefinedObject'"
self isNil "Returns: true"
Scripts can be made executable with a shebang line:
#!/usr/bin/env harding
| sum |
sum := 0
1 to: 100 do: [:i | sum := sum + i]
sumchmod +x script.hrd
./script.hrd# Create a class with slots (no accessors)
Point := Object derive: #(x, y)
# Create a class with public slots (auto accessors + :: access)
Person := Object derivePublic: #(name, age)
# Canonical form with explicit read/write slot lists (v0.8.0+)
Account := Object derive: #(balance, owner)
read: #(balance, owner)
write: #(balance)
# With multiple inheritance (v0.8.0+)
ColoredPoint := Object derive: #(color, x, y)
read: #(color, x, y)
write: #(color, x, y)
superclasses: #(Point)
# Create an instance
p := Point new
p x: 100
p y: 200Instance variables declared with derive: are accessed by name within methods:
Point>>moveBy: dx and: dy [
x := x + dx # Direct slot access
y := y + dy
^ self
]Harding provides several methods for generating getters and setters:
Creates a class and auto-generates both getters and setters for all slots:
Person := Object derivePublic: #(name, age)
p := Person new
p name: "Alice" # Auto-generated setter
p age: 30 # Auto-generated setter
p name # Auto-generated getter - returns "Alice"
p age # Auto-generated getter - returns 30For each slot x, two methods are generated:
x- Getter method that returns the slot valuex:- Setter method that takes one argument and assigns it to the slot
Creates a class with selective accessor generation:
# Generate getters for both slots, but setter only for 'name'
Person := Object derive: #(name, age)
read: #(name, age)
write: #(name)
p := Person new
p name: "Alice" # Works - setter generated
p name # Works - getter generated, returns "Alice"
p age # Works - getter generated, returns nil
p age: 30 # Error - no setter generated for 'age'This is useful when you want:
- Read-only slots (include in read but not write)
- Write-only slots (include in write but not read)
- Public getters with private setters (convention: only generate setters for internal use)
Generated accessors use SlotAccessNode for O(1) direct slot access:
- Getter: Direct slot read by index
- Setter: Direct slot write by index
This provides the same performance as manually written accessor methods that use direct slot access.
# Single inheritance
ColoredPoint := Point derive: #(color)
# Multi-level inheritance
Shape3D := ColoredPoint derive: #(depth)Mixin is a slotless class designed for behavior composition. It derives from Root (alongside Object) and carries no slots, which avoids diamond-problem conflicts when mixed into other classes.
# Create a mixin
Comparable := Mixin derive
# Add methods to it
Comparable >> < other [ ^ (self compareTo: other) < 0 ]
Comparable >> > other [ ^ (self compareTo: other) > 0 ]
Comparable >> between: min and: max [
^ (self >= min) and: [ self <= max ]
]
# Mix into any class
Point := Object derive: #(x, y)
Point addSuperclass: Comparable
# Implement the required method
Point >> compareTo: other [
^ ((x * x) + (y * y)) - ((other x * other x) + (other y * other y))
]
# Now Point supports <, >, between:and:, etc.Harding has a three-tier class hierarchy rooted in Root:
Root (top - zero methods, used for DNU proxies/wrappers)
├── Object # The "working" base class with all standard methods
│ └── All regular classes (String, Integer, Array, etc.)
│
└── Mixin # Slotless sibling for behavior composition (no slots)
└── Mixins like Comparable, Iterable, Printable
- Root: The absolute base of the hierarchy. Has zero methods. Only used internally for DNU proxies and as the parent of Object and Mixin.
- Object: The standard base class for all regular classes. Provides methods like
clone,printString,initialize,=,==, etc. - Mixin: A special sibling to Object for behavior composition. Can be added to any class via
addSuperclass:without affecting instance type.
The core and process libraries provide these mixins in lib/core/Comparable.hrd, lib/core/Equatable.hrd, lib/core/Iterable.hrd, lib/core/Printable.hrd, and lib/process/Synchronizable.hrd:
| Mixin | Requires | Provides |
|---|---|---|
Comparable |
compareTo: |
<, <=, >, >=, between:and:, min:, max:, clampTo:max: |
Equatable |
compareTo: |
=, ~= |
Iterable |
do: |
collect:, select:, reject:, detect:, inject:into:, anySatisfy:, allSatisfy:, noneSatisfy:, count:, sum |
Printable |
printOn: |
printString, print, printCr, displayString |
Synchronizable |
— | critical:, acquire, release |
Combining Comparable and Equatable: Use both mixins together when you need both ordering and equality. Comparable provides ordering operators (<, >, etc.) while Equatable provides equality operators (=, ~=). Both require compareTo: to be implemented.
# Combine several mixins
MyCollection := Object derive: #(items)
MyCollection addSuperclass: Iterable
MyCollection addSuperclass: Printable
MyCollection >> do: block [
items do: block
]
MyCollection >> printOn: stream [
stream show: "MyCollection(".
stream show: items size printString.
stream show: " items)"
]Inside methods, slots are accessed directly by name. This provides O(1) performance compared to named property access.
Performance comparison (per 100k ops):
- Direct slot access: ~0.8ms
- Named slot access: ~67ms
- Property bag access: ~119ms
Slot-based access is 149x faster than property bag access.
The :: operator provides direct O(1) access to slots, table entries, and library bindings without method call overhead.
For slots declared in the read: or write: lists (or all slots with derivePublic:):
# Reading slots
name := person::name
x := point::x
# Writing slots (only if declared writable)
person::name := "Alice"
point::x := 100
# Inside methods - direct slot access (no :: needed)
Person>>haveBirthday [
age := age + 1 # Direct slot access within the class
]table := #{"name" -> "Alice", "age" -> 30}
# Reading
name := table::name
age := table::"age" # String keys work too
# Writing
table::name := "Bob"
table::city := "NYC" # Creates new key if doesn't existMyLib := Library new.
MyLib at: "MyClass" put: SomeClass.
# Access binding directly
cls := MyLib::MyClass- Performance: O(1) direct access without method dispatch
- Conciseness:
obj::slotvsobj slotorobj slot: value - Flexibility: Works for slots, tables, and libraries uniformly
The canonical form for class creation uses explicit read/write slot lists:
# All slots readable and writable (equivalent to derivePublic:)
Person := Object derive: #(name, age)
read: #(name, age)
write: #(name, age)
# Read-only age, read-write name
Person := Object derive: #(name, age)
read: #(name, age)
write: #(name)
# With multiple inheritance
Child := Object derive: #(x)
read: #(x)
write: #(x)
superclasses: #(Parent1, Parent2)Note: derivePublic: is shorthand for derive:read:write: with all slots in both lists.
In Harding, Class is a regular class just like any other. It exists as a global binding and provides class-related functionality, but unlike Smalltalk-80, there are no metaclasses.
Class is the class that describes class objects themselves:
Object class # Returns: Object (a class)
Object class class # Returns: Object (still Object, not a metaclass)
# Class is accessible as a global
Class # Returns: the Class class
Harding at: "Class" # Also returns: the Class classUnlike Smalltalk-80 where every class is an instance of a unique metaclass, in Harding:
- Classes are objects - They can receive messages and have methods
- No metaclasses - Classes are not instances of Class; they are their own class
- Class methods are stored directly on the class object itself
# In Smalltalk-80:
# Object is an instance of Object class (a metaclass)
# Object class is an instance of Metaclass
#
# In Harding:
# Object is a class object
# Object's class is Object itself (not a separate metaclass)
# Class methods are stored on Object directlyClass methods (factory methods) are defined using class>> syntax but are simply methods on the class object:
# Instance method - sent to instances
Person>>greet [ ^ "Hello, " & name ]
# Class method - sent to the class itself
Person class>>newNamed: aName [
| person |
person := self new. # self is the Person class here
person name: aName.
^ person
]Harding simplifies the object model by eliminating metaclasses:
- Simpler mental model - Classes are objects with methods, period
- No metaclass explosion - Creating a class doesn't create a parallel metaclass hierarchy
- Easier implementation - No need to manage metaclass lifecycles
The trade-off is that you cannot override class behavior per-class (like you could with metaclasses in Smalltalk), but this is rarely needed in practice.
While isKindOf: Class doesn't work as you might expect from Smalltalk (since classes aren't instances of Class), you can check if something is a class using:
# Check if a value is a class by checking if it's in the global namespace
# and behaves like a class (can create instances, has methods, etc.)
# Get class name
Object className # Returns: "Object"
# Check class relationship
obj isKindOf: Object # Returns: true if obj inherits from ObjectA class can have multiple parent classes:
# Create two parent classes
Parent1 := Object derive: #(a)
Parent1 >> foo [ ^ "foo1" ]
Parent2 := Object derive: #(b)
Parent2 >> bar [ ^ "bar2" ]
# Create a child that inherits from both
Child := Object derive: #(x)
Child addSuperclass: Parent1
Child addSuperclass: Parent2
# Child now has access to both foo and bar
c := Child new
c foo # Returns "foo1"
c bar # Returns "bar2"When adding multiple parents (via derive: with multiple parents or addSuperclass:), Harding checks for:
Slot name conflicts: If any slot name exists in multiple parent hierarchies, an error is raised.
Parent1 := Object derive: #(shared)
Parent2 := Object derive: #(shared)
Child := Object derive: #(x)
Child addSuperclass: Parent1
Child addSuperclass: Parent2 # Error: Slot name conflictMethod selector conflicts: If directly-defined method selectors conflict between parents, an error is raised.
Parent1 := Object derive: #(a)
Parent1 >> foo [ ^ "foo1" ]
Parent2 := Object derive: #(b)
Parent2 >> foo [ ^ "foo2" ]
Child := Object derive: #(x)
Child addSuperclass: Parent1
Child addSuperclass: Parent2 # Error: Method selector conflictTo work with conflicting parent methods, override the method in the child class first, then use addSuperclass::
Parent1 := Object derive: #(a)
Parent1 >> foo [ ^ "foo1" ]
Parent2 := Object derive: #(b)
Parent2 >> foo [ ^ "foo2" ]
# Create child with override first
Child := Object derive: #(x)
Child >> foo [ ^ "child" ]
# Add conflicting parents - works because child overrides
Child addSuperclass: Parent1
Child addSuperclass: Parent2
(Child new foo) # Returns "child"Note: Only directly-defined methods on each parent are checked for conflicts. Inherited methods (like derive: from Object) will not cause false conflicts.
As of v0.8.0, multiple inheritance uses first-parent-wins lookup order instead of failing on conflicts:
- The class's own methods
- First parent's methods (and its parents)
- Second parent's methods (and its parents)
- And so on...
If the same method exists in multiple parents, the first parent's version is used. A warning is printed for conflicting selectors.
Parent1 := Object derive: #(a)
Parent1 >> foo [ ^ "parent1" ]
Parent2 := Object derive: #(b)
Parent2 >> foo [ ^ "parent2" ]
# No error - first parent wins
Child := Object derive: #(x)
Child addSuperclass: Parent1
Child addSuperclass: Parent2
(Child new foo) # Returns "parent1" (with warning about conflict)To explicitly select which parent's method to use, define an override or use qualified super sends.
Query selector conflicts programmatically:
# Get conflicting selectors between this class and all parents
conflicts := Child conflictSelectors
# Get conflicting selectors with a specific parent class
conflicts := Child classConflictSelectors: Parent2Harding supports both qualified and unqualified super sends for multiple inheritance:
# Unqualified super (uses first parent)
Employee>>calculatePay [
base := super calculatePay.
^ base + bonus
]
# Qualified super (explicit parent selection)
Employee>>calculatePay [
base := super<Person> calculatePay.
^ base + bonus
]# Unary method
Person>>greet [ ^ "Hello, " & name ]
# Method with one parameter
Person>>name: aName [ name := aName ]
# Method with multiple keyword parameters
Point>>moveX: dx y: dy [
x := x + dx.
y := y + dy.
^ self
]Define multiple methods in a single block:
Person extend: [
self >> greet [ ^ "Hello, " & name ].
self >> name: aName [ name := aName ].
self >> haveBirthday [ age := age + 1 ]
]Define factory methods on the class object:
Person extendClass: [
self >> newNamed: n aged: a [
| person |
person := self derive.
person name: n.
person age: a.
^ person
]
]
# Usage
p := Person newNamed: "Alice" aged: 30Create a class with slots AND define methods in one expression:
Person := Object derive: #(name, age) methods: [
self >> greet [ ^ "Hello, I am " & name ].
self >> haveBirthday [ age := age + 1 ]
]# Unary (no arguments)
obj size
obj class
# Binary (one argument, operator)
3 + 4
5 > 3
"a" & "b" # String concatenation
# Keyword (one or more arguments)
dict at: key put: value
obj moveBy: 10 and: 20Binary selectors are made from operator punctuation rather than identifier characters. The common built-in forms are:
+ - * / < > = == % // \\ <= >= ~= << & |
Preferred meanings in current Harding:
&generic concatenationand:logical AND with short-circuit|logical OR
, is reserved for collection/object literal separators and is no longer used
as a binary message selector.
Send multiple messages to the same receiver:
obj
at: #x put: 0;
at: #y put: 0;
at: #z put: 0Use ^ (caret) to return a value:
Point>>x [ ^ x ]If no ^ is used, the method returns self.
Send messages dynamically using symbols:
obj perform: #clone # Same as: obj clone
obj perform: #at:put: with: #x with: 5 # Same as: obj at: #x put: 5# Block with no parameters
[ statements ]
# Block with parameters
[ :param | param + 1 ]
# Block with temporaries
[ | temp1 temp2 |
temp1 := 1.
temp2 := temp1 + 1 ].
# Block with parameters and temporaries
[ :param1 :param2 | temp1 temp2 | code ]The | separator marks the boundary between parameters/temporaries and the body.
Harding-specific feature: Unlike most Smalltalk implementations, Harding allows you to omit the | when a block has parameters but no temporaries:
# Harding - valid and concise
[ :x | x * 2 ]
# Traditional style also works
[ :x | | x * 2 ]Blocks capture variables from their enclosing scope:
value := 10
block := [ value + 1 ] # Captures 'value'
block value # Returns 11Blocks that capture the same variable share access to it:
makeCounter := [ |
count := 0.
^[ count := count + 1. ^count ]
].
counter := makeCounter value.
counter value. # Returns 1
counter value. # Returns 2
counter value. # Returns 3Blocks are invoked via the value: message family:
value- invoke with no argumentsvalue:- invoke with 1 argumentvalue:value:- invoke with 2 arguments- etc.
Use ^ within a block to return from the enclosing method:
findFirst: [ :arr :predicate |
1 to: arr do: [ :i |
elem := arr at: i.
(predicate value: elem) ifTrue: [ ^elem ] "Returns from findFirst:"
].
^nil
](x > 0) ifTrue: ["positive"] ifFalse: ["negative"]
(x isNil) ifTrue: ["nil"]# Times repeat
5 timesRepeat: [Stdout writeline: "Hello"]
# To:do:
1 to: 10 do: [:i | Stdout writeline: i]
# To:by:do:
10 to: 1 by: -1 do: [:i | Stdout writeline: i]
# While
[condition] whileTrue: [body]
[condition] whileFalse: [body]
# Repeat
[body] repeat # Infinite loop
[body] repeat: 5 # Repeat N times# Do: - iterate over collection
collection do: [:each | each print]
# Collect: - transform each element
collection collect: [:each | each * 2]
# Select: - filter elements
collection select: [:each | each > 5]
# Detect: - find first matching
collection detect: [:each | each > 5]
# Inject:into: - fold/reduce
#(1, 2, 3, 4) inject: 0 into: [:sum :each | sum + each]Harding provides exception handling through the on:do: mechanism:
[ protectedBlock ] on: ExceptionClass do: [ :ex | handlerBlock ]Example:
[ "Hello" / 3 ] on: Error do: [ :ex |
Transcript showCr: "Error occurred: " + ex message
]When caught, exception objects have:
message- The error message stringstackTrace- String representation of the call stack
[ riskyOperation ] on: Error do: [ :ex |
Transcript showCr: "Message: " + ex message.
Transcript showCr: "Stack: " + ex stackTrace
]Use signal: to raise an exception:
someCondition ifTrue: [
Error signal: "Something went wrong"
]Division by zero signals a DivisionByZero exception:
# Integer division
result := [ 10 // 0 ] on: DivisionByZero do: [ :ex |
ex resume: 0 # Return 0 instead
]
# Float division
result := [ 10.0 / 0.0 ] on: DivisionByZero do: [ :ex |
"Cannot divide by zero!" println
ex resume: 42
]Harding supports Smalltalk-style resumable exceptions. When an exception is signaled, the signal point is preserved so execution can be resumed from the handler:
[ Error signal: "recoverable" ] on: Error do: [ :ex |
ex resume # Resume from signal point, signal returns nil
]
[ Error signal: "recoverable" ] on: Error do: [ :ex |
ex resume: 42 # Resume from signal point, signal returns 42
]Additional handler methods:
ex retry- Re-execute the protected block from the beginningex return: value- Return value from theon:do:expressionex pass- Delegate to the next matching outer handlerex isResumable- Returns true for resumable exceptions
If no handler matches, Harding runs the exception's default action, prints an uncaught-exception header and stack trace, and exits with a non-zero status.
pass follows the same behavior when there is no outer matching handler.
Errorand its subclasses are treated as non-resumable by default.Notificationis intended for resumable signals.
For debugging, exception handlers can inspect the signal point:
[ Error signal: "oops" ] on: Error do: [ :ex |
ex signaler # The object that signaled the exception
ex signalContext # The activation context at signal point
ex signalActivationDepth # Activation stack depth at signal point
]ensure: currently runs cleanup after normal completion of the protected block:
[ riskyOperation ] ensure: [ cleanup ]It does not yet guarantee cleanup after Harding exceptions or non-local returns.
# ifError: catches Error and its subclasses
[ riskyOperation ] ifError: [ :ex | "fallback" ]Exception
├── Error
├── Notification # Resumable notification (not an error)
├── MessageNotUnderstood
├── SubscriptOutOfBounds
└── DivisionByZero
Parent classes catch subclass exceptions: on: Exception do: catches Error.
Notification is used for resumable signals that represent notifications rather than errors. Handlers can resume from a Notification to continue normal execution.
| Feature | Harding | Smalltalk |
|---|---|---|
| Implementation | VM work queue (stackless) | Custom VM mechanism |
| Stack unwinding | Signal point preserved via ExceptionContext | Immediate |
| Resume capability | Yes (resume, resume:) |
Yes |
Harding provides a Library class for organizing code into isolated namespaces. Libraries allow you to group related classes and avoid polluting the global namespace.
# Create a new library
MyLib := Library new.
# Convenience constructor
MyLib := Library name: "MyLib".
# Add bindings (classes, constants, etc.)
MyLib at: "MyClass" put: SomeClass.
MyLib at: "Constant" put: 42.
# Retrieve bindings
MyLib at: "Constant" # Returns 42
MyLib includesKey: "MyClass" # Returns true
MyLib keys # Returns array of all binding namesThe Library>>load: message loads a file and captures new global definitions into the library's bindings, rather than polluting the global namespace:
# mylib.hrd - defines classes like MyClass, UtilityClass, etc.
MyLib := Library new.
MyLib load: "mylib.hrd"
# The classes from mylib.hrd are in MyLib's bindings
MyLib at: "MyClass" # Returns the class
MyLib at: "UtilityClass" # Returns the class
# They are NOT in the global namespace
Harding includesKey: "MyClass" # Returns falseSuccessive load: calls on the same library evaluate with that library's existing
bindings visible, so later files can reference names defined by earlier ones.
If the library defines __sourceDir, relative paths passed to load: are resolved
from that directory first.
Import a library to make its bindings accessible for name resolution:
MyLib := Library new.
MyLib load: "mylib.hrd"
Harding import: MyLib
# Now classes from MyLib are accessible by name
Instance := MyClass new.
Value := UtilityClass doSomething.import: currently adds the library to a lookup stack; it does not copy bindings
into the receiver. Importing a library that conflicts with an already imported
library now raises an error instead of silently changing lookup precedence.
When resolving a variable name, Harding searches in this order:
- Local scope (temporaries, captured variables, method locals)
- Instance variables (slots on
self) - Imported Libraries (most recent first, if there are no name conflicts)
- Global table (fallback)
Important: Each method activation has its own isolated local scope. Methods cannot see the local variables of their calling method (unlike some dynamic languages). This prevents accidental coupling and ensures proper encapsulation.
Imports with overlapping exported names are rejected:
Lib1 := Library new.
Lib1 at: "SharedKey" put: 1.
Lib2 := Library new.
Lib2 at: "SharedKey" put: 2.
Harding import: Lib1.
Harding import: Lib2. # Error: import conflict between Lib2 and Lib1: SharedKeyThe Standard Library is pre-loaded with common classes and utilities:
Harding load: "lib/core/Set.hrd" # Set
Harding load: "lib/core/Exception.hrd" # Exception hierarchy
Standard load: "lib/standard/Interval.hrd" # Interval
Standard load: "lib/standard/File.hrd" # File convenience API
Standard load: "lib/standard/FileStream.hrd" # Stream I/OCore and Standard are loaded at startup, so these classes are accessible by default:
Set new # Set collection (from Core)
1 to: 10 # Interval (from Standard)
File readAll: "README.md"
Error error: "oops" # Exception class (from Core)Global system utilities are also available by default:
System arguments # Array of CLI args passed after '--'
System cwd # Current working directory
System stdin # Standard input stream
System stdout # Stdout stream
System stderr # Stderr streamHarding supports packaging Nim primitive implementations together with embedded .hrd sources.
Use this flow:
- Define Harding-facing methods in
.hrdusing<primitive ...>selectors. - Implement the selectors in Nim and register them on the target class.
- Bundle all package
.hrdfiles as embedded strings and install them withHardingPackageSpec.
For a full end-to-end example, see docs/NIM_PACKAGE_TUTORIAL.md.
Harding includes a library management system for installing third-party packages:
harding lib list # List available libraries from registry
harding lib fetch # Refresh metadata from remote repositories
harding lib info mysql # Show detailed info about a library
harding lib info sqlite # Show detailed info about SQLite supportharding lib install mysql # Install latest version
harding lib install mysql@1.0.0 # Install specific version
harding lib install sqlite # Install SQLite libraryLibraries are installed to the external/ directory and compiled into Harding on the next build.
harding lib installed # List installed libraries
harding lib update mysql # Update a specific library
harding lib update sqlite # Update the SQLite library
harding lib update --all # Update all libraries
harding lib remove mysql # Remove a library
harding lib remove sqlite # Remove the SQLite libraryAfter installing or updating libraries:
nimble harding # Rebuild with new librariesSQLite is a good fit for local storage and tests because it works with file
paths and ":memory:" databases instead of a separate database server.
conn := SqliteConnection open: ":memory:".
conn execute: "CREATE TABLE players (name TEXT, score INTEGER)".
conn execute: "INSERT INTO players (name, score) VALUES ('Ada', 1200)".
result := conn query: "SELECT name, score FROM players ORDER BY score DESC".
row := result next.
((row at: 0) & " => " & (row at: 1)) println.
result close.
conn close.External libraries are Nim packages that extend Harding:
- Create a Git repository named
harding-<libname> - Add a
<libname>.nimblefile with metadata - Implement Nim primitives in
src/harding_<libname>/ - Create Harding classes in
lib/<libname>/ - Add a
lib/<libname>/Bootstrap.hrdfile
See external/README.md for the full specification.
To load code directly into the global namespace (for method extensions, etc.):
Harding load: "lib/core/Object.hrd"This is used in lib/core/Bootstrap.hrd to load core method extensions before loading new classes into the Standard Library.
Harding supports cooperative green processes:
# Fork a new process
process := Processor fork: [
1 to: 10 do: [:i |
Stdout writeline: i
Processor yield
]
]# Process introspection
process pid # Process ID
process name # Process name
process state # State: ready, running, blocked, suspended, terminated
# Process control
process suspend
process resume
process terminate
# Yield current process
Processor yieldready- Ready to runrunning- Currently executingblocked- Blocked on synchronizationsuspended- Suspended for debuggingterminated- Finished execution
Implemented:
- Basic process forking with
Processor fork: - Explicit yield with
Processor yield - Process state introspection (pid, name, state)
- Process control (suspend, resume, terminate)
- Shared globals via
HardingGlobalTable for inter-process communication
Synchronization Primitives:
- Monitor - Mutual exclusion with condition variables
- SharedQueue - Producer-consumer communication
- Semaphore - Counting and binary locks
See examples in lib/core/Monitor.hrd, lib/core/SharedQueue.hrd, and lib/core/Semaphore.hrd
All processes share the same globals and class hierarchy, enabling inter-process communication.
Harding provides a unified syntax for direct primitive invocation.
Both declarative and inline forms use the same keyword message syntax:
# No arguments
<primitive primitiveClone>
# One argument
<primitive primitiveAt: key>
# Multiple arguments
<primitive primitiveAt: key put: value>Use <<primitive>> as the entire method body when a method's sole purpose is to invoke a primitive. Argument names in the primitive tag MUST match the method parameter names exactly, in the same order.
# No arguments
Object>>clone <primitive primitiveClone>
# One argument - parameter name 'key' must match
Object>>at: key <primitive primitiveAt: key>
# Multiple arguments - parameter names must match
Object>>at: key put: value <primitive primitiveAt: key put: value>Use <<primitive>> within a method body when you need to execute Harding code before or after the primitive call. Arguments can be any variable reference: method parameters, temporaries, slots, or computed values.
# Validation before primitive
Array>>at: index [
(index < 1 or: [index > self size]) ifTrue: [
self error: "Index out of bounds: " + index asString
].
^ <primitive primitiveAt: index>
]
# Using temporary variable
Object>>double [
| temp |
temp := self value.
^ <primitive primitiveAt: #value put: temp * 2>
]- Single syntax to learn - No confusing distinction between
primitive:>andprimitive - Explicit arguments - Arguments are visible in both declarative and inline forms
- Consistent with Smalltalk - Uses keyword message syntax everywhere
- Better validation - Argument names and counts are validated for declarative forms
- More efficient - Bypasses
perform:machinery
For declarative primitives:
- Argument names in the primitive tag must match method parameter names exactly
- Argument order must match parameter order
- Argument count must match the number of colons in the primitive selector
Smalltalk:
x := 1.
y := 2.Harding:
x := 1.
y := 2.
# OR
x := 1
y := 2Smalltalk:
'Hello World' "Single quotes"Harding:
"Hello World" "Double quotes only"Note: Single quotes are reserved for future use.
Smalltalk:
"Double quotes for comments"Harding:
# Hash for commentsSmalltalk: Every class is an instance of a metaclass.
Harding: Classes are objects, but there are no metaclasses. Class methods are stored directly on the class object. The global Class exists for introspection but classes are not instances of it.
# Instance method
Person>>greet [ ^ "Hello" ]
# Class method (no metaclass needed)
Person class>>newPerson [ ^ self new ]See The Class Object section for detailed explanation.
Harding supports multiple inheritance with conflict detection, unlike Smalltalk's single inheritance.
Child := Object derive: #(x)
Child addSuperclass: Parent1
Child addSuperclass: Parent2Smalltalk: Primitives are VM-specific numbered operations.
Harding: Primitives embed Nim code directly using unified syntax.
Object>>at: key <primitive primitiveAt: key>Smalltalk: nil is a special primitive value.
Harding: nil is a singleton instance of UndefinedObject:
nil class # Returns UndefinedObject
nil isNil # Returns trueHarding includes a compiler called Granite that compiles Harding source to native binaries via Nim.
Compile any .hrd script directly:
# Compile to Nim source
granite compile script.hrd
# Build native binary
granite build script.hrd
# Build and run
granite run script.hrd
# Build with optimizations
granite run script.hrd --releaseExample script (sieve.hrd):
primeCount := 0
i := 2
[ i <= 500 ] whileTrue: [
isPrime := true
d := 2
[ d * d <= i ] whileTrue: [
(i \\ d = 0) ifTrue: [
isPrime := false
d := i
].
d := d + 1
].
isPrime ifTrue: [
primeCount := primeCount + 1
].
i := i + 1
].
primeCount printlnThe compiler generates Nim code with:
- Inline control flow:
ifTrue:,ifFalse:,whileTrue:,whileFalse:,timesRepeat:become native Nimif/while/for - Direct variable access (no hash table lookups for local variables)
- Runtime value boxing via
NodeValuevariant type - Arithmetic and comparison helper functions
Granite supports a special syntax for organizing compiled code:
Harding compile: [
# Class and method definitions go here
Dog := Object derivePublic: #(name, age)
Dog>>bark [ ^ "Woof!" ]
]
Harding main: [
# Runtime code goes here
dog := Dog new
dog name: "Buddy"
dog bark println
]
How it works:
Harding compile:- Code that runs at compile time to define classes and methods. These definitions are included in the generated Nim code.Harding main:- Code that becomes the main() procedure (executed at runtime).
Why use this?
- Organization - Separates class definitions from runtime code
- Clarity - Makes it explicit what's compile-time vs runtime
- Multiple inheritance - Shows the order of addSuperclass: calls clearly
Backward compatible: You can also write without these blocks - all top-level code becomes main().
Interpreter behavior: In the interpreter, both Harding compile: and Harding main: evaluate their block immediately (normal block semantics).
Granite behavior: In Granite, Harding compile: is executed during compilation to construct classes/methods, while Harding main: is compiled into generated main() and executed at program runtime.
For building applications from within the Harding VM:
MyApp := Application derive: #()
MyApp>>main: args [
Stdout writeline: "Hello from compiled app!"
^0
]
app := MyApp new
app name: "myapp"
Granite build: appargs receives host command-line arguments in both interpreter and compiled execution paths.
Compiled code runs significantly faster than interpreted. On a sieve of Eratosthenes benchmark (primes up to 5000):
| Mode | Time | Speedup |
|---|---|---|
| Interpreter (debug) | ~23s | 1x |
| Interpreter (release) | ~2.3s | 10x |
| Compiled (release) | ~0.01s | 2300x |
- First-class blocks (blocks assigned to variables or passed as arguments) are not yet compiled
- Non-local returns (
^) from blocks are not yet supported in compiled code - Class/method compilation from in-VM code is in progress
Several Smalltalk-80 features are not implemented:
- Class Variables - Use globals or closures as workarounds
- Class Instance Variables - Not implemented
- Pool Dictionaries - Use global tables or symbols
- Method Categories - Methods stored in flat table
- Change Sets - File-based source with git
- Refactoring Tools - Basic text editing only
- Debugger - VSCode DAP support implemented; GTK IDE debugger in progress
| Precedence | Construct | Associativity |
|---|---|---|
| 1 (highest) | Primary expressions (literals, (), blocks) |
- |
| 2 | Unary messages | Left-to-right |
| 3 | Binary operators | Left-to-right |
| 4 | Keyword messages | Right-to-left (single message) |
| 5 (lowest) | Cascade (;) |
Left-to-right |
# Binary operators
3 + 4 # Addition
5 - 3 # Subtraction
x * y # Multiplication
a / b # Division
x > y # Greater than
x < y # Less than
x = y # Assignment or value comparison
x == y # Equality comparison
x ~= y # Inequality
a <= b # Less than or equal
a >= b # Greater than or equal
a // b # Integer division
a \ b # Modulo
a ~~ b # Not identity
"a" & "b" # String concatenation (non-destructive, returns new string)
"Value: " & 42 # Auto-converts to string: "Value: 42"
s << "text" # In-place append (mutates string, returns self)
a and: [ b ] # Logical AND with short-circuit
a | b # Logical ORHarding provides two complementary ways to work with strings:
The & operator creates new strings:
str1 := "Hello".
str2 := str1 & " World". # str1 is still "Hello", str2 is "Hello World"
result := "A" & "B" & "C". # Creates intermediate strings: "A" -> "AB" -> "ABC"Use for: Simple concatenation, functional style, short strings.
The << operator mutates the string efficiently and supports chaining:
# Efficient string building with pre-allocated capacity
buffer := String withCapacity: 1000.
buffer << "<html>".
buffer << "<head><title>" << title << "</title></head>".
buffer << "<body>" << content << "</body>".
buffer << "</html>".
html := buffer. # The string itself is the result (no .contents needed)Key differences:
<<mutates the string in-place (O(1) amortized with pre-allocated capacity)&creates new strings each time (O(n) per operation)<<returnsselffor method chainingwithCapacity:hints the initial buffer size to avoid reallocations
Use for: Building large strings (HTML, JSON, logs), tight loops, streaming output.
Harding currently provides three JSON entry points:
json{...}/Json{...}/JSON{...}prefix literals for inline JSON stringsJson parse:to parse JSON text into Harding valuesJson stringify:to convert Harding values into compact JSON text
The Json class is available by default.
Prefix JSON literals return JSON text directly:
payload := json{"status": "ok", "count": 42}
payload class # Stringjson{} supports full JSON syntax:
# Objects with nested structures
json{
"person": {
"name": "Alice",
"age": 30,
"address": {"city": "NYC", "zip": "10001"}
}
}
# Arrays using JSON [1, 2, 3] syntax
json{
"items": [1, 2, 3],
"tags": ["a", "b", "c"],
"matrix": [[1, 2], [3, 4]]
}
# Arrays of objects
json{
"users": [
{"name": "Alice", "id": 1},
{"name": "Bob", "id": 2}
]
}
# Mixed with variables
Name := "Todo API".
Count := 42.
json{
"name": Name,
"count": Count,
"tags": ["api", "rest"]
}Key features:
- Native JSON object syntax:
{"key": value} - JSON array syntax:
[1, 2, 3](not#(1, 2, 3)) - Nested objects and arrays
- Dynamic values via variables and expressions
- Commas separate items (not
->arrows)
These literals are useful for:
- inline API payloads
- tests and fixtures
- OpenAPI specifications
- small constant JSON documents
Json parse: converts JSON text into Harding values:
value := Json parse: "{\"name\":\"Alice\",\"age\":30,\"tags\":[\"a\",\"b\"]}"Current conversions:
| JSON | Harding |
|---|---|
| object | Table |
| array | Array |
| string | String |
| integer | Integer |
| float | Float |
| true / false | Boolean |
| null | nil |
Example:
user := Json parse: "{\"name\":\"Alice\",\"admin\":true,\"scores\":[1,2,3]}".
user at: "name" # "Alice"
user at: "admin" # true
user at: "scores" # #(1, 2, 3)If parsing fails, Json parse: currently returns nil.
Json stringify: converts Harding values into compact JSON text:
Json stringify: #{"a" -> 1, "b" -> #(2, 3, 4)}Current supported inputs:
- Numbers
- Strings
- Booleans
nil- Arrays
- Tables
- ordinary Harding objects
- Array and Table instances
Example:
payload := #{
"name" -> "Alice".
"age" -> 30.
"roles" -> #("admin", "editor").
"active" -> true
}.
jsonText := Json stringify: payload.Output is compact JSON with no pretty-print formatting.
Ordinary Harding objects now serialize as JSON objects using slot order by default:
Person := Object derivePublic: #(name, age).
person := Person new.
person::name := "Alice".
person::age := 30.
Json stringify: person
# => {"name":"Alice","age":30}Inherited slots are included as well.
Harding supports declarative class-side JSON configuration for common cases:
User := Object derivePublic: #(id, username, password, avatarUrl)
; jsonExclude: #(password)
; jsonRename: #{#username -> "userName"}
; jsonOmitNil: #(avatarUrl).Supported class-side messages:
jsonExclude:jsonOnly:jsonRename:jsonOmitNil:jsonOmitEmpty:jsonFormat:jsonFieldOrder:jsonResetjsonSpec
Supported built-in formatter symbols:
#string#rawJson#symbolName#className
Example with formatting:
Envelope := Object derivePublic: #(payload, kind).
Envelope jsonFormat: #{#payload -> #rawJson, #kind -> #symbolName}.When declarative slot-based serialization is not enough, a class can define jsonRepresentation.
The method should return a Harding value that Json stringify: already understands, typically a Table, Array, primitive, or nested combination of those.
Invoice := Object derivePublic: #(id, lines).
Invoice>>jsonRepresentation [
^ #{
"id" -> id,
"lineCount" -> lines size
}
]This path is more flexible, but slower than the compiled slot-plan path.
Also note:
- JSON table keys support Strings, Symbols, Numbers, Booleans,
nil, Classes, and primitive wrapper instances; unsupported key types raise an error - There is no built-in pretty printer yet
- Parse errors currently return
nilrather than signaling a JSON-specific exception - Arbitrary transformer blocks are not part of the current fast-path API
As of the current implementation, Json stringify: writes directly into a mutable string buffer inside the primitive and serializes ordinary objects through compiled class plans.
This improves the core serialization path for primitives, Arrays, Tables, and ordinary slot-based objects.
There are benchmark scripts for the current JSON support:
nim c -r tests/benchmark_json_stringify.nim
nim c -r tests/benchmark_json_objects.nimThey compare:
- Harding
Json stringify:on a reasonably complex nested Table/Array payload - Harding
Json stringify:on a reasonably complex nested object graph - an equivalent pure Nim direct string writer
- an equivalent pure Nim
std/jsonbuilder
Recent release-build results on the current implementation were approximately:
- Table/Array payload: Harding ~129.72 ms, Nim direct ~19.75 ms, Nim
std/json~91.40 ms - Object graph payload: Harding ~34.48 ms, Nim direct ~11.40 ms, Nim
std/json~37.92 ms
The object-graph case is the more relevant comparison for the new slot-plan serializer. In that benchmark, Harding is already slightly faster than Nim std/json, while still behind a hand-written direct Nim encoder.
Harding provides several built-in types that form the foundation of the object system:
The number hierarchy follows Smalltalk conventions with a common base class:
Object
└─ Number # Base class for all numeric types
├─ Integer # Whole numbers
└─ Float # Floating-point numbers
Both Integer and Float inherit common methods from Number:
# Common Number methods (available on both Integer and Float)
abs # Absolute value
negated # Negation (0 - self)
squared # Square (self * self)
between:and: # Check if value is in range
isZero # Self equals 0?
isPositive # Self > 0?
isNegative # Self < 0?
sign # -1, 0, or 1# Arithmetic
i + j
i - j
i * j
i / j # Regular division
i // j # Integer division
i * 1.0 # Automatic promotion to Float
# Modulo
i % j # Modulo operator
i \ j # Alternative modulo
# Comparison
i = j
i < j
i > j
i <= j
i >= j
i <> j # Not equal
# Parity
i even # Is even?
i odd # Is odd?
# Iteration
i timesRepeat: [ :k | k printString ] # Execute block i times
i to: 10 do: [ :k | k printString ] # Iterate from i to 10
1 to: 10 by: 2 do: [ :k | k printString ] # Step iteration
# Special methods
i factorial # i!
i gcd: j # Greatest common divisor
i lcm: j # Least common multiple
i sqrt # Square root (returns Float)# Arithmetic (same operators as Integer)
f + g
f - g
f * g
f / g
f // g # Integer division of float values
# Comparison
f = g
f < g
f > g
f <= g
f >= g
f <> g
# Special methods
f abs
f negated
f sqrt # Square rootObject
└─ Boolean
├─ True
└─ False
Both True and False inherit from Boolean, which supports common boolean operations:
b ifTrue: [ ... ]
b ifFalse: [ ... ]
b ifTrue: [ ... ] ifFalse: [ ... ]
b ifFalse: [ ... ] ifTrue: [ ... ]
b and: [ ... ] # Logical AND with short-circuit
b or: [ ... ] # Logical OR with short-circuit
b not # Negation# Strings (double quotes only)
"hello" # String literal
str size # Length
str at: 0 # Character at index
str1 & str2 # Non-destructive concatenation (returns new String)
buffer := String withCapacity: 100. # Pre-allocated string for efficient building
buffer << "part1" << "part2". # In-place append (returns self for chaining)
# Arrays (0-based indexing)
#(1, 2, 3) # Array literal
arr at: 0 # First element
arr at: 0 put: 99 # Set element
arr size
arr add: 42
# Tables (dictionaries)
#{"a" -> 1, "b" -> 2}
dict at: "key"
dict at: "new" put: value
dict keys
# Blocks
[ :x | x + 1 ] # Block with parameter
block value: 10 # Invoke block
[ 1 + 2 ] value # Block with no args# Create array
arr := #(1, 2, 3)
arr := Array new: 5 # Empty array with 5 slots
# Access (0-based indexing)
arr at: 0 # First element
arr at: 0 put: 10 # Set first element
# Methods
arr size # Number of elements
arr add: 4 # Append element
arr join: "," # Join with separator# Create table
dict := #{"key" -> "value", "foo" -> "bar"}
# Access
dict at: "key"
dict at: "newKey" put: "newValue"
# Methods
dict keys # All keys
dict includesKey: "key" # Check if key existsInstall the external bitbarrel library and rebuild Harding.
BarrelTable - Hash-based persistent key-value storage:
# Load BitBarrel library
load: "lib/bitbarrel/Bootstrap.hrd".
# Create persistent table
users := BarrelTable create: "users".
users at: 'alice' put: 'Alice Smith'.
name := users at: 'alice'.
# Collection operations
users keys. # All keys
users size. # Number of entries
users includesKey: 'alice'. # Check existence
users removeKey: 'alice'. # Remove entry
# Iterate
users do: [:key :value |
Transcript showCr: key + " => " + value
].
# Select/Collect (returns in-memory Table)
adults := users select: [:key :user | (user at: 'age') >= 18].
names := users collect: [:key :user | user at: 'name'].BarrelSortedTable - Ordered storage with range queries:
# Create ordered table (uses critbit index)
logs := BarrelSortedTable create: "logs".
logs at: '2024-01-01:001' put: 'System started'.
# Range queries (returns in-memory Table)
janLogs := logs rangeFrom: '2024-01-01' to: '2024-02-01'.
# Prefix queries
day1Logs := logs prefix: '2024-01-01:'.
# Ordered access
logs first. # First entry
logs last. # Last entry
logs keys. # Keys in sorted orderInstall BitBarrel:
./harding lib install bitbarrel
nimble harding- QUICKREF.md - Quick syntax reference
- BOOTSTRAP.md - Bootstrap architecture and core loading
- GTK.md - GTK integration and GUI development
- IMPLEMENTATION.md - VM internals and architecture
- TOOLS_AND_DEBUGGING.md - Tool usage and debugging
- COMPILATION_PIPELINE.md - Granite pipeline architecture
- ROADMAP.md - Active development roadmap
- PERFORMANCE.md - Performance workflow and priorities
- FUTURE.md - Future plans and roadmap
- VSCODE.md - VSCode extension
- research/ - Historical design documents