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.

VariantCommandWhat you get
Library (default)beamtalk new foobeamtalk.toml, src/Foo.bt, test/FooTest.bt
Applicationbeamtalk new foo --appAbove 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:

TargetCommandDescription
buildbeamtalk buildCompile the project
testbeamtalk testRun the test suite
fmtbeamtalk fmt --checkCheck formatting
fmt-fixbeamtalk fmtFormat in place
cifmt build testFull CI check
release(script)Tag a release from beamtalk.toml version
publishgit push origin --tagsPush release tags
runbeamtalk runRun 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:

  1. Pass 1 (Discovery) — Read each .bt source file and parse it into an AST. Extract class names, superclass relationships, and method signatures. This produces the class_module_index (class name to BEAM module name) and class_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.

  2. 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 .bt file produces a .core file in the build directory.

  3. BEAM compilation (erlc) — Invoke OTP's compile module to turn each .core file into a .beam file. 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:

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:

ModeModule nameExample
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:

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:

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

CommandNative equivalentDescription
:load path/to/file.btWorkspace 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 CounterCounter reloadRecompile a class from its source file
:reload(reload last loaded file/dir)Reload the most recently loaded file or directory
:sync / :sWorkspace syncSync 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

CommandNative equivalentDescription
:help / :h / :?(none)Show REPL help message
:help CounterBeamtalk help: CounterShow class documentation and methods
:help Counter incrementBeamtalk help: Counter selector: #incrementShow 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

CommandNative equivalentDescription
:test / :tWorkspace testRun all loaded test classes
:test CounterTestWorkspace test: CounterTestRun 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

CommandDescription
:clearClear all variable bindings
:exit / :quit / :qExit 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:

MethodDescription
versionBeamtalk version string
allClassesAll registered class names (always up-to-date)
classNamed: #CounterLook up a class by name
globalsSnapshot of system namespace as a Dictionary
help: CounterFormatted class documentation
help: Counter selector: #incrementFormatted 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:

MethodDescription
load: "path"Compile and hot-load a file or directory
syncSync workspace with project (beamtalk.toml)
classesAll loaded user classes
testClassesAll loaded test classes
globalsSnapshot of project namespace as a Dictionary
actorsAll live actors
actorAt: pidStringLook up an actor by pid string
actorsOf: CounterAll live instances of a class
testRun all test classes
test: CounterTestRun a specific test class
bind: value as: #NameRegister a value in the workspace namespace
unbind: #NameRemove 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>
  1. Session locals — per-connection variables (x := 42), created by := assignment
  2. Workspace user bindings — workspace-level names registered via Workspace bind:as:
  3. Workspace globals — project-level entries (Transcript, loaded classes, singletons)
  4. 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 ToolDescription
evaluateRun any Beamtalk expression in the workspace
load_fileCompile and hot-reload a .bt file
load_projectLoad all project files in dependency order
list_modulesList loaded BEAM modules
list_classesList loaded Beamtalk classes (with optional filter)
docsQuery class/method documentation
testRun tests (all, by class, or by file)
search_examplesSearch the bundled example corpus
search_classesSearch class names and documentation
enable-tracingEnable detailed trace event capture for actor dispatch
disable-tracingDisable trace event capture (aggregate stats remain)
get-tracesRetrieve trace events with optional filters (actor, selector, class, outcome, duration)
export-tracesExport trace events to a JSON file
actor-statsGet 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:

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

MethodDescriptionExample
assert:Assert condition is trueself assert: (x > 0)
assert:equals:Assert two values are equalself assert: result equals: 42
deny:Assert condition is falseself deny: list isEmpty
should:raise:Assert block raises errorself should: [1 / 0] raise: #badarith
fail:Unconditional failureself 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:

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.