Tooling
Tooling is part of the language, not an afterthought. Beamtalk is designed to be used interactively — by humans and AI agents alike — through a live workspace that all tools connect to over the same WebSocket protocol.
CLI Tools
# Project management
beamtalk new mylib # Create new library project
beamtalk new myapp --app # Create new application project (supervisor + Main)
beamtalk build # Compile to BEAM
beamtalk run # Compile and start
beamtalk check # Check for errors without compiling
# Dependencies (see Package Management guide)
beamtalk deps add json --git https://github.com/jamesc/beamtalk-json --tag v1.0.0
beamtalk deps list # Show resolved dependencies
beamtalk deps update # Update lockfile
# Development
beamtalk repl # Interactive REPL (connects to workspace)
beamtalk test # Run test suite
# Native Erlang
beamtalk gen-native MyActor # Generate skeleton gen_server from a native: Actor class
For full details on beamtalk.toml, dependencies, lockfiles, qualified names, and collision detection, see the Package Management guide.
Project Types
beamtalk new creates a library by default — a package with source and tests, no application supervisor. Pass --app to create an application project with a supervisor and Main entry point.
| Variant | Command | What you get |
|---|---|---|
| Library (default) | beamtalk new foo | beamtalk.toml, src/Foo.bt, test/FooTest.bt |
| Application | beamtalk new foo --app | Above plus [application] in toml, src/FooAppSup.bt, src/Main.bt |
Both variants generate a Justfile, .github/workflows/ci.yml, AGENTS.md, and .mcp.json.
Standard Justfile Targets
Every generated project includes a Justfile with these targets:
| Target | Command | Description |
|---|---|---|
build | beamtalk build | Compile the project |
test | beamtalk test | Run the test suite |
fmt | beamtalk fmt --check | Check formatting |
fmt-fix | beamtalk fmt | Format in place |
ci | fmt build test | Full CI check |
release | (script) | Tag a release from beamtalk.toml version |
publish | git push origin --tags | Push release tags |
run | beamtalk run | Run the application (--app only) |
Build System
beamtalk build compiles a project (or a single .bt file) to BEAM bytecode. The build pipeline is incremental — unchanged files are skipped, and the compiler caches class metadata across builds.
Build Phases
Every build runs through three sequential phases:
-
Pass 1 (Discovery) — Read each
.btsource file and parse it into an AST. Extract class names, superclass relationships, and method signatures. This produces theclass_module_index(class name to BEAM module name) andclass_superclass_index(class to superclass) needed for cross-file resolution in Pass 2. Dependency validation (stdlib reservation checks, native module collision detection) also runs here. -
Pass 2 (Compile) — For each changed source file: run semantic analysis (type checking, protocol conformance, dependency warnings), then generate Core Erlang. The full class-module index from Pass 1 is injected so cross-file class references resolve correctly. Each
.btfile produces a.corefile in the build directory. -
BEAM compilation (
erlc) — Invoke OTP'scompilemodule to turn each.corefile into a.beamfile. This is the standard Erlang compiler — Beamtalk delegates BEAM bytecode generation entirely to OTP.
Incremental Compilation
Beamtalk uses mtime-based change detection to avoid redundant work:
- Each
.btsource file is compared against its corresponding.beamoutput. If the source is newer (or no.beamexists), the file is recompiled. - Pass 1 caching — The class-module index from Pass 1 is cached in
_build/dev/ebin/.beamtalk-pass1-cache.json. On subsequent builds, only files whose source has changed since the cache was written are re-parsed. Unchanged files reuse their cached class metadata. If thebeamtalk.tomlmanifest changes, the entire cache is invalidated and all files are re-parsed. - Force rebuild —
beamtalk build --forceignores all caches and recompiles everything. Useful after toolchain upgrades or when build artifacts may be stale (e.g. switching git branches or worktrees). - Orphan detection —
.beamfiles with no corresponding.btsource are reported as warnings (the source may have been deleted or renamed).
Build Artifact Layout (BuildLayout)
All build output goes under _build/ in the project root. The BuildLayout struct centralises path construction so all commands (build, test, run, repl, deps) use consistent locations.
<project_root>/
_build/
dev/
ebin/ — compiled .beam files from .bt sources
.beamtalk-pass1-cache.json — Pass 1 incremental cache
native/
ebin/ — compiled .beam files from native .erl sources
include/ — generated headers (e.g. beamtalk_classes.hrl)
default/lib/ — rebar3 hex dependency libs (ERL_LIBS)
rebar.config — generated rebar3 config (when hex deps exist)
deps/
<name>/ — git dependency checkout
ebin/ — compiled .beam files for the dependency
For single-file builds (no beamtalk.toml), output goes to build/ alongside the source file instead of _build/dev/ebin/.
Module Naming
Beamtalk uses a namespaced BEAM module naming scheme to avoid collisions in the flat BEAM module namespace:
| Mode | Module name | Example |
|---|---|---|
Package (beamtalk.toml present) | bt@<package>@<relative_path> | bt@myapp@Counter |
| Single file (no manifest) | bt@<module> | bt@Counter |
Subdirectories within src/ are reflected in the module name: src/models/User.bt in package myapp becomes bt@myapp@models@User.
Dependency Compilation Order
When a project has dependencies (declared in beamtalk.toml), the build resolves and compiles the full transitive dependency graph in topological order before compiling the project's own sources. Each dependency's class-module index is merged into the project's index so cross-package class references resolve during codegen.
Native Erlang dependencies (.erl files under native/ or hex packages) are compiled after Pass 1 but before Pass 2. This ordering ensures the generated beamtalk_classes.hrl header (which maps Beamtalk class names to BEAM module names) is available for native .erl files to include.
__beamtalk_meta/0 Protocol
Every compiled Beamtalk class module exports a __beamtalk_meta/0 function — a zero-process reflection entry point that returns a map of class metadata without requiring any gen_server process to be running:
Module:'__beamtalk_meta'() ->
#{
class => 'Counter',
superclass => 'Actor',
meta_version => 2,
is_sealed => false,
is_abstract => false,
is_value => false,
fields => [count],
field_types => #{count => 'Integer'},
methods => [{increment, 1}, {value, 0}],
class_methods => [{new, 0}],
method_info => #{...},
class_method_info => #{...}
}
This metadata serves multiple purposes:
- Incremental builds: The compiler server caches
__beamtalk_meta/0output for all loaded classes, injecting it into each compilation request so the type checker can validate cross-class method calls on user-defined classes (see ADR 0050). - Crash recovery: When the compiler port restarts, the server scans all loaded BEAM modules and repopulates its class cache from
__beamtalk_meta/0— no persistent state files needed. - Tooling: Debuggers, documentation generators, and the reflection API (
Beamtalk allClasses,Beamtalk help:) can query class structure without going through a class process.
Hot-Patching
When a .bt file is reloaded (via Workspace load:, :reload, or a VS Code file save), the compiler produces new BEAM bytecode and the runtime hot-loads it using standard OTP code loading (code:load_binary/3). The class gen_server process updates its live state to reflect the new methods — hot-patched methods take effect on the next message send without restarting the actor.
The compiler server is also notified of the change so its class cache stays current. Since __beamtalk_meta/0 is a compiled function baked into the module, it reflects the original compilation — not hot-patches. The gen_server's class_state record is the authoritative live view; the compiler server receives updates synthesised from class_state, not from the stale __beamtalk_meta/0.
Embedded Compiler Architecture
The Beamtalk compiler (beamtalk-core, written in Rust) runs as an OTP Port managed by beamtalk_compiler_server — a supervised gen_server inside the BEAM node (ADR 0022). This replaces the older separate-daemon architecture and provides:
- Supervised lifecycle — The compiler port is restarted automatically on crash. No orphaned socket files or manual recovery.
- Cross-platform — OTP Ports work on all platforms (Linux, macOS, Windows). No Unix-specific IPC.
- Stateless port — The Rust compiler binary is a pure function: source code + metadata in, Core Erlang out. All session state (class cache, known variables) lives in the Erlang gen_server and is injected per request via ETF (Erlang Term Format) over length-prefixed frames.
REPL
The Beamtalk REPL is an interactive development environment connected to a persistent workspace — a detached BEAM node that survives disconnections (ADR 0004). Actors keep running between sessions; REPL variable bindings are session-local.
Starting the REPL
beamtalk repl # Start or reconnect to workspace
beamtalk repl --foreground # Start node in foreground (debug mode)
beamtalk repl --port 9090 # Use a specific port
beamtalk repl --node mynode # Use a specific node name
On startup, the REPL prints the version and a help hint:
Beamtalk v0.3.1
Type :help for available commands, :exit to quit.
>
If a workspace is already running (from a previous session or another terminal), the REPL reconnects to it. All previously loaded classes and running actors are still available.
Expression Evaluation
The REPL evaluates any Beamtalk expression and prints the result. Variables assigned in one expression persist across subsequent expressions within the same session.
> x := 42
42
> x + 10
52
> counter := Counter spawn
#Actor<Counter,0.234.0>
> counter increment
1
> counter getValue
1
Actor message sends are auto-awaited in the REPL — when you send a message to an actor, the REPL waits for the reply and prints it. In compiled code, you would need to handle the asynchronous response explicitly.
Variable reassignment works as expected:
> x := 100
100
> x + 10
110
Loop mutations also persist across REPL inputs:
> count := 0
0
> 5 timesRepeat: [count := count + 1]
5
> count
5
Commands Reference
REPL commands start with : and provide shortcuts for common operations. Most are thin wrappers around Beamtalk-native message sends (ADR 0040), so the same operations work from compiled code, scripts, and actor methods.
Code Loading
| Command | Native equivalent | Description |
|---|---|---|
:load path/to/file.bt | Workspace load: "path/to/file.bt" | Compile and hot-load a .bt file |
:load path/to/dir/ | Workspace load: "path/to/dir/" | Load all .bt files from a directory recursively |
:reload Counter | Counter reload | Recompile a class from its source file |
:reload | (reload last loaded file/dir) | Reload the most recently loaded file or directory |
:sync / :s | Workspace sync | Sync workspace with project (requires beamtalk.toml) |
:unload Counter | (removes class) | Unload a user class from the workspace |
:load compiles the file and hot-loads the resulting BEAM module. If a class with the same name already exists, the new code replaces it — live actors pick up the new methods on their next message send.
> :load examples/counter.bt
Loaded: Counter
> c := Counter spawn
#Actor<Counter,0.234.0>
> c increment
1
:load also accepts directories, loading all .bt files recursively:
> :load src/models/
Loaded 3 files from src/models/
:sync loads (or reloads) the entire project defined by beamtalk.toml in the current directory. It is incremental — unchanged files are skipped:
> :sync
Reloaded 2 of 5 files
> :s
Reloaded 0 of 5 files (5 unchanged)
:unload removes a user class from the workspace. Stdlib classes cannot be unloaded:
> :unload MyClass
ok
> :unload Integer
ERROR: Cannot remove stdlib class
Inspection and Documentation
| Command | Native equivalent | Description |
|---|---|---|
:help / :h / :? | (none) | Show REPL help message |
:help Counter | Beamtalk help: Counter | Show class documentation and methods |
:help Counter increment | Beamtalk help: Counter selector: #increment | Show method documentation |
:help Counter class | (class-side docs) | Show class-side methods (spawn, new, reload, ...) |
:help Counter class create: | (class-side method docs) | Show class-side method documentation |
:bindings / :b | (session-local) | Show current variable bindings |
:show-codegen <expr> / :sc <expr> | (REPL-only) | Show generated Core Erlang for an expression |
:help provides interactive documentation lookup:
> :help Integer
== Integer < Number ==
Instance methods:
+ - * / ...
> :help Integer +
== Integer >> + ==
...
> :help Integer isPositive
== Integer >> isPositive ==
(inherited from Number)
...
Package-qualified class names are also supported:
> :help stdlib@Integer
== Integer < Number ==
...
:bindings shows all variables in the current session:
> x := 42
42
> name := "hello"
hello
> :bindings
name = hello
x = 42
:show-codegen displays the Core Erlang output for any expression, useful for understanding what the compiler generates:
> :sc 2 + 3
'erlang':'+'(2, 3)
Testing
| Command | Native equivalent | Description |
|---|---|---|
:test / :t | Workspace test | Run all loaded test classes |
:test CounterTest | Workspace test: CounterTest | Run a specific test class |
> :load test/counter_test.bt
Loaded: CounterTest
> :test CounterTest
Running 1 test class...
✓ testIncrement
✓ testMultipleIncrements
2 passed, 0 failed
Session Control
| Command | Description |
|---|---|
:clear | Clear all variable bindings |
:exit / :quit / :q | Exit the REPL (Ctrl+D also works) |
:clear removes all session-local variable bindings:
> x := 42
42
> :clear
ok
> x
ERROR: Undefined variable
Multi-line Input
The REPL automatically detects incomplete input (unclosed brackets, trailing operators, etc.) and shows a ..> continuation prompt. For constructs where the REPL cannot determine completeness from syntax alone — protocol definitions and class definitions without methods — press Enter on a blank line to submit the accumulated input.
> Protocol define: Greetable
..> greet -> String
..> ← blank line submits the definition
Protocol Greetable defined
Class definitions with at least one method (=>) auto-submit. A blank line is only needed for class headers with state declarations only (before you have added methods):
> Actor subclass: Config
..> state: verbose = false
..> ← blank line submits the class
Multi-line expressions work naturally with blocks, collections, and keyword messages:
> [
..> :x |
..> x * 2
..> ] value: 21
42
> #{
..> #name => "Alice",
..> #age => 30
..> } at: #name
Alice
Use Ctrl+C to cancel multi-line input without submitting.
Interactive Class Definitions
Classes can be defined directly at the REPL prompt without writing a .bt file:
> Actor subclass: InlineCounter
..> state: value = 0
..> increment => self.value := self.value + 1
Methods can be added or replaced on existing classes using Tonel-style standalone method definitions (>>):
> InlineCounter >> getValue => self.value
> c := InlineCounter spawn
#Actor<InlineCounter,0.234.0>
> c increment
1
> c getValue
1
Redefining a method replaces it for all future message sends, including on existing actor instances:
> InlineCounter >> increment => self.value := self.value + 10
> c2 := InlineCounter spawn
#Actor<InlineCounter,0.234.0>
> c2 increment
10
Hot Reload
When a class is reloaded (via :load, :reload, or an inline redefinition), existing actor instances pick up the new code on their next message send. State is preserved across the reload.
> :load examples/counter.bt
Loaded: Counter
> c := Counter spawn
#Actor<Counter,0.234.0>
> c increment
1
> c increment
2
// Edit counter.bt to change increment behavior, then:
> :reload Counter
Counter
// Existing actor uses new code, state preserved
> c increment
12
Class-based reload uses the source file path recorded at load time:
> Counter sourceFile
examples/counter.bt
> Counter reload // Recompiles from that path
Counter
> Integer sourceFile // Stdlib classes have no source file
nil
> Integer reload // Cannot reload stdlib
ERROR: Integer has no source file
Workspace and Reflection Singletons
Two global singleton objects provide introspection and project operations. These are available in the REPL, in compiled code, and via the MCP server.
Beamtalk (class: BeamtalkInterface) — system reflection:
| Method | Description |
|---|---|
version | Beamtalk version string |
allClasses | All registered class names (always up-to-date) |
classNamed: #Counter | Look up a class by name |
globals | Snapshot of system namespace as a Dictionary |
help: Counter | Formatted class documentation |
help: Counter selector: #increment | Formatted method documentation |
> Beamtalk version
0.3.1
> Beamtalk allClasses
#(Integer, String, Array, ...)
> Beamtalk classNamed: #Integer
Integer
> Beamtalk globals
#{#Integer => Integer, #String => String, ...}
Workspace (class: WorkspaceInterface) — project operations:
| Method | Description |
|---|---|
load: "path" | Compile and hot-load a file or directory |
sync | Sync workspace with project (beamtalk.toml) |
classes | All loaded user classes |
testClasses | All loaded test classes |
globals | Snapshot of project namespace as a Dictionary |
actors | All live actors |
actorAt: pidString | Look up an actor by pid string |
actorsOf: Counter | All live instances of a class |
test | Run all test classes |
test: CounterTest | Run a specific test class |
bind: value as: #Name | Register a value in the workspace namespace |
unbind: #Name | Remove a workspace binding |
> Workspace load: "examples/counter.bt"
["Counter"]
> Workspace classes
#(Counter, ...)
> Workspace actors
#(#Actor<Counter,0.234.0>, ...)
> Workspace actorsOf: Counter
#(#Actor<Counter,0.234.0>)
> Workspace test
3 passed, 0 failed
> Workspace bind: myActor as: #MyTool
nil
> MyTool
#Actor<Counter,0.234.0>
> Workspace unbind: #MyTool
nil
Variable Persistence and Name Resolution
REPL variable bindings are session-local — each connected REPL session has its own variable scope. Bindings persist across expressions within the same session but are lost when the session disconnects.
Workspace bindings (via Workspace bind:as:) are workspace-level — they persist across sessions and are visible to all connected clients.
Name resolution follows a scoped chain:
Session locals → Workspace user bindings → Workspace globals → Beamtalk globals
x = 42 MyTool = <actor> Transcript = ... Integer = <class>
counter = ... Counter = <class> String = <class>
- Session locals — per-connection variables (
x := 42), created by:=assignment - Workspace user bindings — workspace-level names registered via
Workspace bind:as: - Workspace globals — project-level entries (Transcript, loaded classes, singletons)
- Beamtalk globals — system-level entries (all registered classes, version)
Common Workflows
Load, Edit, Reload, Test
The typical interactive development cycle:
> :load src/Counter.bt // Load the class
Loaded: Counter
> c := Counter spawn // Try it out
#Actor<Counter,0.234.0>
> c increment
1
// ... edit Counter.bt in your editor ...
> :reload Counter // Hot-reload the change
Counter
> c increment // Existing actor uses new code
11
> :load test/CounterTest.bt // Load tests
Loaded: CounterTest
> :test CounterTest // Run tests
2 passed, 0 failed
Project Sync Workflow
For projects with beamtalk.toml, use :sync to load the whole project:
> :sync // Initial load
Reloaded 5 of 5 files
// ... edit files ...
> :s // Incremental reload (short alias)
Reloaded 1 of 5 files (4 unchanged)
> :test // Run all tests
10 passed, 0 failed
Prototyping with Inline Classes
Define and iterate on classes without creating files:
> Actor subclass: Greeter
..> state: name = "World"
..> greet => "Hello, " ++ self.name
> g := Greeter spawn
#Actor<Greeter,0.234.0>
> g greet
Hello, World
// Add a method
> Greeter >> greetWith: prefix => prefix ++ " " ++ self.name
> g greetWith: "Hi"
Hi World
// Replace a method
> Greeter >> greet => "Hey there, " ++ self.name
> g greet
Hey there, World
Actor Lifecycle Management
Inspect, stop, and kill actors through the workspace:
> c := Counter spawn
#Actor<Counter,0.234.0>
> c isAlive
true
> Workspace actors // See all live actors
#(#Actor<Counter,0.234.0>)
> c stop // Graceful shutdown
ok
> c isAlive
false
> c stop // Idempotent
ok
MCP Server (AI Agent Interface)
Beamtalk includes an MCP (Model Context Protocol) server that gives AI coding agents structured access to the live workspace. The MCP tools connect via the same WebSocket protocol as the REPL and VS Code extension — the agent interacts with the same running system.
| MCP Tool | Description |
|---|---|
evaluate | Run any Beamtalk expression in the workspace |
load_file | Compile and hot-reload a .bt file |
load_project | Load all project files in dependency order |
list_modules | List loaded BEAM modules |
list_classes | List loaded Beamtalk classes (with optional filter) |
docs | Query class/method documentation |
test | Run tests (all, by class, or by file) |
search_examples | Search the bundled example corpus |
search_classes | Search class names and documentation |
enable-tracing | Enable detailed trace event capture for actor dispatch |
disable-tracing | Disable trace event capture (aggregate stats remain) |
get-traces | Retrieve trace events with optional filters (actor, selector, class, outcome, duration) |
export-traces | Export trace events to a JSON file |
actor-stats | Get aggregate per-actor, per-method statistics (call counts, durations, error rates) |
All MCP tools map to the same Workspace and Beamtalk APIs available in the REPL. The tracing tools correspond to the Tracing stdlib class (see Language Features — Actor Observability).
VS Code Extension
The Beamtalk VS Code extension connects to the live workspace and provides:
- Syntax highlighting for
.btfiles - LSP integration — diagnostics, completions, go-to-definition
- Workspace tree view — live actors, loaded classes, and session bindings in the sidebar
- Transcript view — real-time output from the workspace
- Inspector panels — inspect live actor state
- Workspace status — connected/disconnected indicator
The extension connects to the same workspace as the REPL and MCP server. Changes made in VS Code (file saves trigger hot-reload) are immediately visible in the REPL and to AI agents.
Testing Framework
Beamtalk includes a native test framework inspired by Smalltalk's SUnit.
// stdlib/test/counter_test.bt
TestCase subclass: CounterTest
testInitialValue =>
self assert: (Counter spawn getValue) equals: 0
testIncrement =>
self assert: (Counter spawn increment) equals: 1
testMultipleIncrements =>
counter := Counter spawn
3 timesRepeat: [counter increment]
self assert: (counter getValue) equals: 3
Each test method gets a fresh instance with setUp → test → tearDown lifecycle.
Assertion Methods
| Method | Description | Example |
|---|---|---|
assert: | Assert condition is true | self assert: (x > 0) |
assert:equals: | Assert two values are equal | self assert: result equals: 42 |
deny: | Assert condition is false | self deny: list isEmpty |
should:raise: | Assert block raises error | self should: [1 / 0] raise: #badarith |
fail: | Unconditional failure | self fail: "not implemented" |
Running Tests
// From the REPL:
> :load stdlib/test/counter_test.bt
Loaded CounterTest
> :test CounterTest
Running 1 test class...
✓ testIncrement
✓ testMultipleIncrements
2 passed, 0 failed
// Equivalent native API — works from compiled code too:
> (Workspace test: CounterTest) failed
// => 0
Parallel Test Runner
By default, beamtalk test runs test classes concurrently using the BEAM scheduler count:
beamtalk test # Auto parallelism (default)
beamtalk test --jobs 4 # Up to 4 classes concurrently
beamtalk test -j 1 # Sequential (backward-compatible)
From code: TestRunner runAll: 0 (auto), TestRunner runAll: 4 (bounded), TestRunner runAll (sequential).
Serial opt-out: Test classes that touch global state (registered names, ETS tables, TranscriptStream) should override serial to prevent interference:
TestCase subclass: TracingTest
class serial -> Boolean => true
testEnable =>
self assert: Tracing isEnabled equals: false
Serial classes run alone, sequentially, after all concurrent classes complete.
Shared Protocol Architecture
All interfaces — REPL, VS Code, MCP — connect to the same live BEAM workspace node via the same WebSocket JSON protocol (beamtalk_ws_handler). This means:
- An agent loading a file via MCP is immediately visible in VS Code and the REPL
- A developer hot-reloading a class in VS Code is immediately available to the agent
- All interfaces share actors, loaded modules, and workspace state
Adding a new interface requires only a protocol adapter over the existing WebSocket transport.
Native Erlang Integration
For packages that need hand-written Erlang code — gen_server implementations, hex.pm dependencies, or direct OTP control — see the Native Erlang Integration guide. It covers the native/ directory layout, the native: keyword for actor classes, the gen-native stub generator, and hex dependency management via [native.dependencies] in beamtalk.toml.