cml
10. Tracing & Instrumentation
CML includes a macro-based tracing system for debugging and performance analysis:
- Zero-overhead when disabled: Tracing is compiled out unless
-Dtraceis passed. - Event IDs: Every event and pick has a unique ID for correlation.
- Fiber context: Trace output includes the current fiber (or user-assigned fiber name).
- Outcome tracing: Commit/cancel outcomes are logged for all key CML operations.
- User-defined tags:
CML.traceaccepts an optionaltag:argument for grouping/filtering. - Flexible output: Trace output can be redirected to any IO (file, pipe, etc) via
CML::Tracer.set_output(io). - Filtering: Tracer can filter by tag, event type, or fiber using
set_filter_tags,set_filter_events, andset_filter_fibers.
Example:
CML.trace "Chan.register_send", value, pick, tag: "chan"
CML::Tracer.set_output(File.open("trace.log", "w"))
CML::Tracer.set_filter_tags(["chan", "pick"])
See src/trace_macro.cr for details.
Crystal Concurrent ML (CML)
A minimal, composable, and correct Concurrent ML runtime for Crystal — built from first principles using events, channels, and fibers.
💡 Concurrent ML (CML) is a message-passing concurrency model introduced by John Reppy. It extends synchronous channels with first-class events that can be composed, chosen, or canceled safely.
1. Overview
This library provides a small but complete CML implementation in pure Crystal. It adds a higher-level event layer on top of Crystal's built-in channels and fibers.
Core features:
Event(T)abstraction for synchronization- Atomic commit cell (
Pick) ensuring only one event in a choice succeeds Chan(T)supporting synchronous rendezvous communication- Event combinators:
choose,wrap,guard,nack,timeout - Fully deterministic, non-blocking registration semantics
- Fiber-safe cancellation and cleanup
Design principles:
- One pick, one commit: Exactly one event in a choice succeeds
- Zero blocking in registration:
try_registernever blocks - Deterministic behavior: Predictable regardless of scheduling
- Memory safe: No recursion in structs, proper cleanup
2. Installation
Add this to your shard.yml:
dependencies:
cml:
github: your-username/cml.cr
Then run:
shards install
3. Quickstart
After/Timeout Helper
CML.after(1.second) { puts "Timeout reached!" }
Spawning a Worker and Waiting for Result
result_evt = CML.spawn_evt { compute_something() }
CML.sync(result_evt)
Pipeline with Channels
ch1 = CML::Chan(Int32).new
ch2 = CML::Chan(String).new
CML.after(0.seconds) { ch1.send(42) }
CML.after(0.seconds) { ch2.send("done") }
CML.sync(CML.choose([ch1.recv, ch2.recv]))
See the Cookbook for more idioms and patterns.
Basic Channel Communication
require "cml"
# Create a channel for integers
ch = CML::Chan(Int32).new
# Spawn a sender
spawn { CML.sync(ch.send_evt(99)) }
# Receiver waits synchronously
val = CML.sync(ch.recv_evt)
puts val # => 99
Racing Events with Timeout
# Use choose to race a receive against a timeout
evt = CML.choose([
ch.recv_evt,
CML.wrap(CML.timeout(1.second)) { |_t| "timeout" }
])
puts CML.sync(evt) # => "timeout" if no message arrives
Event Composition & DSL Helpers
string_evt = CML.wrap(ch.recv_evt) { |x| "Received: #{x}" }
lazy_evt = CML.guard { expensive_computation_evt }
safe_evt = CML.nack(ch.recv_evt) { puts "Event was cancelled!" }
# After/Timeout helper
CML.after(2.seconds) { puts "done after 2s" }
# Spawn a fiber and get result as event
evt = CML.spawn_evt { 123 }
CML.sync(evt) # => 123
4. Core API
Events
CML.sync(evt)- Synchronize on an eventCML.always(value)- Event that always succeedsCML.never- Event that never succeedsCML.timeout(duration)- Time-based eventCML.after(span) { ... }- Run a block after a delay (helper)CML.spawn_evt { ... }- Run a block in a fiber, return result as event (helper)
Combinators
CML.choose(events)- Race multiple eventsCML.wrap(evt, &block)- Transform event resultCML.guard(&block)- Lazy event constructionCML.nack(evt, &block)- Cancellation cleanup
Channels
CML::Chan(T).new- Create a synchronous channelchan.send_evt(value)- Send eventchan.recv_evt- Receive event
5. Advanced Usage
Nested Choices
inner = CML.choose([evt1, evt2])
outer = CML.choose([inner, evt3])
result = CML.sync(outer)
Multiple Concurrent Channels
ch1 = CML::Chan(Int32).new
ch2 = CML::Chan(String).new
evt = CML.choose([
CML.wrap(ch1.recv_evt) { |x| "Number: #{x}" },
CML.wrap(ch2.recv_evt) { |s| "String: #{s}" }
])
Re-entrant Guards
evt = CML.guard do
if some_condition
CML.always(:ready)
else
CML.timeout(1.second)
end
end
6. Documentation
- Overview & Architecture - Deep dive into event semantics
- API Reference - Complete API documentation
- Examples - Working code examples
- Cookbook - Common idioms and patterns
7. Running Tests
crystal spec
8. Contributing
See CONTRIBUTING.md for development guidelines and AGENTS.md for AI agent contribution rules.
9. License
MIT License - see LICENSE file for details.
11. Debugging Guide
See docs/debugging_guide.md for practical tips on using tracing, tags, and filtering to debug slow, stuck, or incorrect code in CML.
cml
- 0
- 0
- 0
- 0
- 0
- 2 days ago
- October 27, 2025
MIT License
Wed, 29 Oct 2025 13:47:46 GMT