Cure v0.27.0 :: See Your System Breathe
by Aleksei Matiushkin
v0.26.0 closed the OTP application loop. You could declare an app,
describe [application] and [release] in Cure.toml, and
package the whole thing as a bootable release with cure release.
What you could not yet do was watch that application breathe once
it was running, or prove that the FSMs it owns satisfy the
live-ness and safety properties you had in mind when you drew
them. v0.27.0 is the observability-and-verification release that
delivers both halves of that story.
The theme is See Your System Breathe. Every piece below either makes a running Cure application observable or makes the language's guarantees surface to the user earlier.
The trajectory
- v0.25.0 -- typed supervision trees (
actor,sup, the Melquiades Operator). - v0.26.0 -- OTP application lifecycle (
app, releases,Std.App). - v0.27.0 -- observability + verification around the running application.
Observability
Three new surfaces make a running Cure release introspectable.
Cure.OTel -- span bridge
Cure.OTel is an OpenTelemetry-compatible span emitter on top of
Cure.Pipeline.Events. It is started explicitly:
Cure.OTel.start(
exporter: &MyApp.Exporter.record/1,
service_name: "my_app",
sample_ratio: 1.0
)
Once started, every event on every pipeline stage becomes a span. Library code can also open manual spans:
Cure.OTel.span("cure.actor.send", %{"inbox" => "Ping.Pong"}, fn ->
pid |> send(message)
end)
Nested spans share a trace id and chain via :parent_span_id.
inject_ctx/1 / extract_ctx/1 carry the span context across
process boundaries; drop the token into the metadata field of a
Melquiades Operator message and the receiver continues the trace.
When opentelemetry_api is loaded, spans are forwarded there.
Otherwise the bundled Cure.OTel.MemoryExporter stashes every
span into a public ETS table (:cure_otel_spans) ready for test
assertions. Zero-runtime-cost when the bridge is not started.
cure top -- snapshot observer
cure top
watch -n1 mix cure.top # low-tech live view
cure top 2026-04-21T15:20:00Z procs=85 reductions=12345
Supervisors (1)
- Cure.Sup.Atelier.Root (2 children)
Actors (2)
- painter_1 (Cure.Actor.Painter) mbox=0 mem=9184 reds=301
- curator_1 (Cure.Actor.Curator) mbox=0 mem=9032 reds=212
FSMs (1)
- exhibit_1 (Cure.FSM.Exhibit) state=Closed events=0 uptime_ms=42
Reads from the live ETS registries owned by Cure.Sup.Runtime,
Cure.Actor.Runtime, and Cure.FSM.Runtime. The snapshot API
(Cure.Observe.Top.snapshot/0 + render/2) is also consumable
from code and from Livebook.
cure trace -- typed tracer
cure trace Cure.Std.List.map/2 --duration 10
Every call and return is formatted through inspect/2 and,
when a compile-time signature is known, annotated with the Cure
type of each argument and the declared effect set:
call #PID<0.212.0> Cure.Std.List.map/2([1, 2, 3] : List(Int), #Function<...>) [pure]
return #PID<0.212.0> Cure.Std.List.map/2 -> [2, 4, 6] : List(Int)
Signatures live in an ETS registry (Cure.Observe.Trace.Registry)
that the type checker can populate during a project compile.
Missing entries fall back to plain inspect/2.
Verification
Two new verification surfaces plug directly into the FSM / actor model.
Cure.Temporal -- LTL over FSMs
Write the properties you care about in a small, readable DSL:
always eventually Open
never Dead
always (Locked -> eventually Unlocked)
Locked until Unlocked
Cure.Temporal.Parser parses; Cure.Temporal.Formula is the LTL
ADT; Cure.Temporal.Checker does the model checking over an
FSM's transition graph:
{:ok, f} = Cure.Temporal.Parser.parse_one("always eventually Open")
model = Cure.Temporal.Checker.from_fsm(
[%{from: "Closed", to: "Open"},
%{from: "Open", to: "Closed"}],
"Closed"
)
Cure.Temporal.Checker.check(f, model)
# => {:ok, :holds}
Safety properties (always P, never P) are checked via
exhaustive BFS over the reachable closure; liveness properties
(eventually P) via forward BFS; P until Q chains both. Failed
properties return a concrete counterexample trace rather than an
opaque "failed" atom. Default bound is length(states) * 8,
overridable via bound: n.
Cure.Protocol -- session types for actors
Declare a session-typed binary protocol between two roles:
protocol Ping.Pong with Client, Server
Client -> Server: Ping
Server -> Client: Pong(Int)
end
Cure.Protocol.Parser, Cure.Protocol.Script, and
Cure.Protocol.Verifier give you:
- Structural role-usage and role-membership checks (
E056). - Reachability of every projected state from the initial state.
Cure.Protocol.Script.project/2to get an FSM-style transition list for a single role -- ready to feed back intoCure.Temporal.Checkerfor liveness properties on the projection.
{:ok, script} = Cure.Protocol.parse_and_verify(dsl)
client_view = Cure.Protocol.project(script, "Client")
client_model = Cure.Protocol.participant_trace(script, "Client")
Typed-hole synthesis
Placeholder ?hole in your source, cure synth suggests the fix:
cure synth --goal Int --ctx "n=Int,xs=List(Int)"
goal: Int
ctx: %{"n" => "Int", "xs" => "List(Int)"}
Candidates:
1. n (cost 1, pure)
2. Std.List.length(xs) (cost 3, pure)
3. n |> Std.Math.abs (cost 3, pure)
Cure.Types.Synth.synthesise/4 walks a depth-budgeted enumeration
of well-typed candidates against a goal type and a local context.
Ranking is shorter-beats-longer, pure-beats-effectful, local-vars-
beat-stdlib, with a seeded catalogue of common stdlib entries
(Std.Core, Std.Option, Std.Result, Std.List, Std.Math,
Std.String, Std.Io). Runs out of budget? The :synthesis stage
emits E061 Synthesis Budget Exhausted and the CLI walks away
with an empty result.
Three new stdlib modules
Std.Time
use Std.Time
fn now_iso() -> String ! Io =
let i = Std.Time.utc_now()
Std.Time.format_iso8601(i)
fn tomorrow(i: Instant) -> Instant =
Std.Time.add(i, Std.Time.hours(24))
Instant (Unix microseconds), Duration, ISO 8601 parser/formatter,
arithmetic, Unix conversions, smart duration constructors. Runtime
is :cure_std_time.
Std.Regex
use Std.Regex
fn valid_email(s: String) -> Bool =
match Std.Regex.compile("^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$")
Ok(r) -> Std.Regex.is_match(r, s)
Error(_) -> false
Thin wrapper over :re: compile, compile_bang, is_match,
run, scan, replace, split. Invalid patterns surface as
the new E060 Regex Invalid.
Std.CRDT
Five state-based CRDTs whose merge/2 operations are commutative,
associative, and idempotent -- enforced by the test suite:
GCounter,PNCounter-- counters.ORSet-- observed-remove set.LWWRegister,MVRegister-- registers.
use Std.CRDT
let painter = Std.CRDT.or_add(Std.CRDT.or_empty(), :painter, :abstract)
let curator = Std.CRDT.or_add(Std.CRDT.or_empty(), :curator, :cubism)
let merged = Std.CRDT.or_merge(painter, curator)
Runtime bridge: :cure_std_crdt.
OSC 8 clickable-filepath errors
Cure.Term.Hyperlink wraps file paths and line numbers in OSC 8
escape sequences when the terminal supports them. The Compiler
errors formatter picks it up for free:
error: type mismatch
--> lib/app.cure:42
| declared return type Int but body has type String
In WezTerm, Kitty, iTerm2, VTE, or Warp the filepath is clickable
and jumps straight to the offending line. NO_COLOR disables
emission; CURE_HYPERLINKS=0 forces it off; CURE_HYPERLINKS=1
forces it on regardless of terminal detection.
Playground
A LiveView Playground lands at
/playground:
- Two-pane editor: plain-text source on the left, Makeup- highlighted HTML preview on the right.
- 150 ms debounced updates so typing feels immediate without saturating the socket.
- Driven by the same
Makeup.Lexers.CureLexerthe REPL uses for its raw-mode highlighter.
Type-checking and sandboxed evaluation are on the roadmap for v0.28; see the Playground page for the full plan.
Auto-generated Mermaid diagrams
Cure.Doc.Mermaid renders any FSM / sup / app container as
Mermaid source ready to embed in cure doc HTML output. An FSM
becomes a stateDiagram-v2 with per-event labels and !/?
suffixes preserved; a supervisor tree becomes a graph TD with
each child's module, restart policy, and shutdown timeout; an
application becomes a graph LR with the root edge and the
declared extra applications.
Error catalog
Five new codes:
- E056 Protocol Violation -- dead role, stranger role, or
unreachable projected state inside a
protocoldeclaration. - E059 Temporal Property Violated -- with a minimal counterexample trace.
- E060 Regex Invalid -- bad pattern surfaced at compile or call time.
- E061 Synthesis Budget Exhausted --
Cure.Types.Synthran out of depth before finding a candidate. - E062 Temporal Target Unknown -- temporal formula references a state absent from the model.
cure explain E0NN prints the full text with examples.
examples/cure_atelier/
The canonical v0.27.0 showcase. A compact Mix project whose test suite exercises every new surface end to end:
Std.CRDT.ORSetfor painter and curator tag sets merged without coordination.Std.Time.Instantfor curation timestamps.Std.Regexfor input validation with capture groups.Cure.Protocolverifies anAtelier.Gallerysession type and surfacesE056on a deliberately broken variant.Cure.Temporalspecifiesalways eventually Openandnever Openover anExhibitFSM.Cure.OTel+ theMemoryExportercapture nested span chains.Cure.Types.Synthsuggests a fix for a hole.Cure.Term.Hyperlinkis exercised through the shared error formatter.
Read
examples/cure_atelier/test/cure_atelier_test.exs
for the most compact tour of the release.
Getting started
git clone https://github.com/am-kantox/cure-lang.git
cd cure
mix deps.get && mix test
mix escript.build
./cure version # Cure 0.27.0
./cure top # runtime snapshot
./cure synth --goal Int --ctx "n=Int,xs=List(Int)"
Read the new references on-disk: docs/OBSERVABILITY.md,
docs/TEMPORAL.md,
docs/PROTOCOL.md,
docs/PLAYGROUND.md,
the extended docs/STDLIB.md,
and the full CHANGELOG.
Numbers
- 1451 tests pass (up from 1114 pre-release; 3 doctests + 1448 tests).
- 0
mix credo --strictissues across 238 source files. - 34 stdlib modules compile clean (up from 31).
What's next
The Playground's type-checker half plus the WASM target sit at the top of the v0.28 queue. The long-term radar from v0.24.0 / v0.25.0 / v0.26.0 is unchanged: monomorphisation, profile-guided optimisation, broader IDE reach, REPL-level hot reload. The repository is at github.com/am-kantox/cure-lang.