A Smalltalk-inspired message-passing system for Bash.
Trashtalk implements message passing, inheritance, traits, aspect-oriented programming and persistent instances - with bash.
I'm not a big fan of bash. I think POSIX is the computing environment we deserve, not the one we need. Bash's ubiquity is its strongest selling point, so strong in fact that bash scripting remains the more-or-less correct choice for a lot of situations, especially in my line of work. This really gets my goat.
I've seen others twist bash/sh into strange loops to give themselves superpowers - both in-person and from afar: a few small tricks, conventions or utilities can become a force-multiplier for software authorship. Personal software authorship. Trashtalk started as a minimal message-passing implementation in bash, intended as an experiment in the direction of enabling expressive personal tool-making in the ugly substrate of shell-scripting.
It lingered in my dotfiles repo for years.
Then LLMs came. I said "Hey Claude, what do you think about this gewgaw over here?" Claude said "You're absolutely right!" and we were off - it morphed into a DSL transpiled into bash, then I added a compiler written in golang to provide native compilation for a subset of the DSL, then I started trying to add a TUI-based Smalltalk-style IDE on top of it. Things continued to get weirder and weirder, each day I travelled half the distance between here and v1.0, and eventually it dawned on me that I'd gone too far, so I dialed back Trashtalk and jettisoned the non-bash bits. More precisely, I spun them off into their own projects. Anyhow, here we are.
So far I've only really used Trashtalk to work on Trashtalk. I'll let you know when that changes. Until then, some things I'm thinking about doing include:
- Exploring the idea of an acme-like editor as a substitute for the whiz-bang TUI I tried so desperately to make work
- Improving the SQLite instance persistence layer, maybe adding some kind of superadjacent analytical layer using duckdb
If you have any ideas that aren't terribly rude, I'd love to hear them!
Trashtalk uses a DSL compiler that transforms Smalltalk-inspired source files (.trash) into namespaced Bash functions. This way, we implement message passing without polluting the global namespace. Or, well, we pollute it in a principled fashion.
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Source (.trash)│────▶│ Compiler │────▶│ Compiled (bash) │
│ │ │ │ │ │
│ Counter subclass│ │ jq-compiler │ │ __Counter__ │
│ method: inc │ │ │ │ increment() │
└─────────────────┘ └──────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Dispatcher │
│ │
│ @ Counter inc │
│ ▼ │
│ __Counter__ │
│ increment() │
└─────────────────┘
- DSL Compiler (
lib/jq-compiler/) - jq-based two-pass compiler that transforms.trashsource files into executable Bash - Dispatcher (
lib/trash.bash) - Routes@message sends to the appropriate namespaced function - Source Files (
trash/*.trash) - Human-readable class definitions - Compiled Files (
trash/.compiled/) - Generated Bash code (also copied totrash/for runtime)
Clone or copy this repository to ~/.trashtalk:
git clone <repo-url> ~/.trashtalkAdd the following to your .bashrc or .zshrc:
source ~/.trashtalk/lib/trash.bash# Send a message to an object
@ Trash info
# Create a counter instance
counter=$(@ Counter new)
@ $counter setValue 5
@ $counter increment 3
@ $counter show
# Create an array
arr=$(@ Array new)
@ $arr push hello
@ $arr push world
@ $arr show
# System introspection
@ Trash listObjects
@ Trash methodsFor Counter
@ Trash helpClasses are defined in .trash files using a Smalltalk-inspired syntax:
# Counter - A simple counter class
Counter subclass: Object
include: Debuggable
instanceVars: value:0 step:1
method: increment [
| newValue |
newValue := $(( $(_ivar value) + $(_ivar step) ))
_ivar_set value "$newValue"
echo "$newValue"
]
method: setValue: val [
_ivar_set value "$val"
]
method: show [
echo "Counter value: $(_ivar value)"
]| Element | Syntax | Description |
|---|---|---|
| Class declaration | ClassName subclass: SuperClass |
Declare a class with inheritance |
| Trait declaration | TraitName trait |
Declare a trait (mixin) |
| Include trait | include: TraitName |
Mix in a trait |
| Instance variables | instanceVars: name:default |
Declare instance vars with defaults |
| Dependencies | requires: 'path/to/file.bash' |
Source external dependencies |
| Method | method: name [body] |
Define an instance method |
| Method with args | method: foo: x bar: y [body] |
Keyword-style arguments |
| Class method | classMethod: name [body] |
Define a class method |
| Raw method | rawMethod: name [body] |
Pass-through (no transformation) |
| Test method | testMethod: name [body] |
Define an inline test (see Testing) |
| Local variables | | var1 var2 | |
Declare local variables |
| Assignment | var := value |
Assign to variable |
| Self reference | @ self methodName |
Message to self |
The compiler transforms DSL constructs to Bash:
# DSL syntax:
method: example: arg [
| result |
result := $(some_command)
@ self debug: "Got result: $result"
@ OtherClass doSomething: "$result" with: "$arg"
]
# Compiles to:
__MyClass__example() {
local arg="$1"
local result
result=$(some_command)
@ "$_RECEIVER" debug "Got result: $result"
@ OtherClass doSomething_with "$result" "$arg"
}Use rawMethod: for code that shouldn't be transformed (heredocs, traps, complex bash):
rawMethod: createConfig: name [
cat > "$CONFIG_DIR/$name" << 'EOF'
# Configuration file
setting=value
EOF
echo "Created config: $name"
]Traits provide reusable behavior without inheritance:
Debuggable trait
method: debug: message [
[[ "${TRASH_DEBUG:-1}" == "0" ]] && return 0
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] DEBUG ($_RECEIVER): $message" >&2
]
method: inspect [
echo "Object: $_RECEIVER"
echo "Class: $_SUPERCLASS"
]Trashtalk supports before/after advice for cross-cutting concerns like logging, validation, or notifications:
Account subclass: Object
instanceVars: balance:0
method: withdraw: amount [
balance := balance - amount
]
method: deposit: amount [
balance := balance + amount
]
# Run before withdraw: executes
before: withdraw: do: [
@ self log: "Attempting withdrawal"
]
# Run after deposit: completes
after: deposit: do: [
@ self notifyBalanceChanged
]Advice hooks execute automatically - before:do: runs prior to the method, after:do: runs after it returns.
Trashtalk supports defining tests directly in class files using testMethod:. Tests run automatically when using @ Trash edit: ClassName - if tests fail, you're returned to the editor.
Counter subclass: Object
instanceVars: value:0 step:1
method: increment [
value := value + step.
^ value
]
method: setStep: s [
step := s
]
testMethod: testIncrement [
pragma: primitive
local c result
c=$(@ Counter new)
result=$(@ "$c" increment)
_assert_eq "$result" "1" "increment returns 1"
@ "$c" destroy
]
testMethod: testCustomStep [
pragma: primitive
local c
c=$(@ Counter new)
@ "$c" setStep: 5
_assert_eq "$(@ "$c" increment)" "5" "custom step works"
@ "$c" destroy
]Tests use TAP (Test Anything Protocol) assertions:
| Function | Description |
|---|---|
_assert_eq "$actual" "$expected" "desc" |
Assert values are equal |
_assert_neq "$actual" "$unexpected" "desc" |
Assert values are not equal |
_assert_true "$value" "desc" |
Assert value is non-empty |
_assert_false "$value" "desc" |
Assert value is empty |
_assert_contains "$haystack" "$needle" "desc" |
Assert string contains substring |
_assert_ok "command" "desc" |
Assert command succeeds (exit 0) |
# Run tests for a class
@ Trash runTestsFor: Counter
# Check if a class has tests
@ Trash hasTestsFor: Counter
# Tests run automatically during edit flow
@ Trash edit: CounterOutput follows TAP format:
# Running tests for Counter
ok 1 - increment returns 1
ok 2 - custom step works
1..2
# All 2 tests passed
Compile a single class:
make single CLASS=MyClassCompile all classes:
make compileOr use the compiler directly:
lib/jq-compiler/driver.bash compile trash/MyClass.trash > trash/.compiled/MyClassTrashtalk includes a built-in profiling system to help identify performance bottlenecks and optimize method dispatch.
Set TRASH_PROFILE=1 to enable profiling output:
# Profile to stderr
TRASH_PROFILE=1 @ Counter new
# Profile to a file
TRASH_PROFILE=1 TRASH_PROFILE_FILE=profile.log @ MyApp runProfiling logs entry and exit points with timing:
[1767909948.119] → Counter.new [native]
[daemon] Counter.new 44ms route=fallback reason=no_plugin
[1767909948.248] → Counter.new [native→bash]
[1767909948.295] ← Counter.new [native→bash] 153ms
→marks method entry←marks method exit with elapsed time- Route types:
native,bash,native→bash,bash:direct
| Variable | Description |
|---|---|
TRASH_PROFILE=1 |
Enable profiling output |
TRASH_PROFILE_FILE=path |
Write to file instead of stderr |
TRASH_PROFILE_DEPTH=N |
Only log calls up to depth N |
TRASH_PROFILE_MIN_MS=N |
Only log calls taking >= N milliseconds |
Use bin/trash-profile-analyze to generate reports from profile logs:
# Generate profile data
TRASH_PROFILE=1 @ MyApp run 2>profile.log
# Analyze the profile
bin/trash-profile-analyze profile.logThe analyzer generates a report showing:
- Dispatch Routing: Breakdown of native vs bash execution
- Slowest Methods: Top 10 methods by execution time
- Most Called Methods: Top 10 methods by call count
- Classes by Call Count: Which classes are used most
- Recommendations: Suggestions for optimization (e.g., classes that would benefit from native plugins)
Example output:
================================================================================
TRASHTALK PROFILE REPORT
================================================================================
Run duration: 2.5 seconds
Total method calls: 150
Total method time: 2340ms
DISPATCH ROUTING
----------------
[native→bash] 120 calls ( 80%) avg 15ms total 1800ms
[bash] 30 calls ( 20%) avg 18ms total 540ms
SLOWEST METHODS (top 10)
------------------------
153ms Dictionary.new [native→bash]
89ms Array.map [bash]
...
RECOMMENDATIONS
---------------
1. Dictionary has 45 calls but no native support - prioritize for dylib
| Class | Description |
|---|---|
Object |
Root class with new, findAll, count methods |
Trash |
System introspection and management |
Store |
SQLite-backed instance persistence |
Array |
Dynamic array with push, pop, map, filter |
Counter |
Simple counter with increment/decrement |
File |
File system operations (read, write, temp files) |
Future |
Async computation with result retrieval |
Process |
External OS process management (subprocess-like) |
ReplServer |
Socket-based REPL server for Emacs integration |
| Trait | Description |
|---|---|
Debuggable |
Debug logging, inspection, ancestry tracing |
# Basic syntax
@ <Receiver> <selector> [args...]
# Examples
@ Trash info # No arguments
@ Counter new # Returns instance ID
@ $counter increment 5 # Instance method with arg
@ Store getField_field "$id" name # Keyword method (compiled form)Instances are stored in SQLite via the Store class:
# Create and persist
counter=$(@ Counter new)
@ $counter setValue 42
# Find later
@ Counter findAll # List all Counter instances
@ Counter find "value > 10" # Query with predicate
@ Counter count # Count instancesVendored in lib/vendor/:
sqlite-json.bash- SQLite-based JSON document store and key-value persistencetuplespace/- Event coordinationbsfl.sh- Bash utility functionsfun.sh- Functional programming utilities
External tools (install separately):
jo- JSON output from shelljq- JSON processorsqlite3- Database engineuuidgen- UUID generation (usually pre-installed)
Trashtalk includes a major mode for Emacs with syntax highlighting, indentation, and REPL integration for interactive development.
Add to your init.el:
(add-to-list 'load-path "~/.trashtalk/emacs")
(require 'trashtalk-mode)Or with use-package:
(use-package trashtalk-mode
:load-path "~/.trashtalk/emacs"
:mode "\\.trash\\'")TODO Think we nuked it The REPL server provides interactive evaluation, hot reloading, and introspection from Emacs.
Start the server in a terminal:
@ ReplServer startConnect from Emacs with C-c C-z in any .trash buffer.
TODO Lord have mercy this is too much
| Key | Command | Description |
|---|---|---|
C-c C-c |
trashtalk-eval-defun |
Evaluate method at point |
C-c C-r |
trashtalk-eval-region |
Evaluate selected region |
C-c C-l |
trashtalk-eval-line |
Evaluate current line |
C-c C-b |
trashtalk-eval-buffer |
Evaluate entire buffer |
C-c C-k |
trashtalk-reload-current-file |
Recompile and reload class |
C-c C-z |
trashtalk-repl-connect |
Connect to REPL server |
C-c C-i |
trashtalk-info-at-point |
Show info for symbol at point |
C-c C-m |
trashtalk-methods-for-class |
List methods for a class |
TODO Didn't we kill this??? Shit should we bring it back?
The server uses a simple line-based protocol over a Unix socket (/tmp/trashtalk-repl.sock):
Request: COMMAND:payload
Response: STATUS:result
Commands: EVAL, COMPLETE, INFO, METHODS, RELOAD, PING, QUIT
You can also interact with the server from the command line:
echo "PING" | nc -U /tmp/trashtalk-repl.sock
echo "EVAL:@ Counter new" | nc -U /tmp/trashtalk-repl.sock~/.trashtalk/
├── emacs/
│ └── trashtalk-mode.el # Emacs major mode with REPL support
├── lib/
│ ├── trash.bash # Main runtime & dispatcher
│ ├── jq-compiler/ # jq-based DSL compiler
│ │ ├── driver.bash # CLI entry point
│ │ ├── tokenizer.bash # Source → JSON tokens
│ │ ├── parser.jq # Tokens → AST
│ │ └── codegen.jq # AST → Bash code
│ └── vendor/ # Vendored dependencies
├── trash/
│ ├── *.trash # DSL source files
│ ├── .compiled/ # Compiled output
│ │ └── traits/ # Compiled traits
│ └── traits/ # Trait source files
└── tests/ # Test scripts
Supposedly v1.0.0
Chaz Straney
