← All posts

Cure v0.30.0 :: John

by Aleksei Matiushkin

release diagnostics cli repl observability tribute

v0.29.0 was the release where documentation caught up with the language it describes. cure doc grew a proper two-pane site, every stdlib module learnt to talk about itself, the REPL's :help stopped butchering its own Markdown, and the parser stopped choking on doc comments separated by blank lines. With the reader experience in order, v0.30.0 turns to the operator.

The headline feature is tiny on the surface and a little obsessive underneath. It is called john, it prints everything, and it is named in tribute to John Carbajal, one of the most intelligent and friendly persons I was honored to work with.

Why John

John is a former colleague of mine. He had one talent that I spent a long time trying to acquire: on a screen full of numbers, he would put his finger on the one number that mattered, usually while the rest of us were still trying to get a header to stop wrapping.

You did not have to explain the dashboard to him. You had to give him the dashboard. He would then tell you what the dashboard was about.

Over the years I came to an uncomfortable conclusion. The reason this felt so hard to replicate was that most of our dashboards did not show enough. We curated them. We hid the uncomfortable numbers. We left Z3 out, left the scheduler count out, left the log tail out. We did not trust each other with the full picture, so we served half-pictures back and forth.

The john command is the admission that we were wrong. It shows everything it can -- version, scheduler topology, memory breakdown, process and atom counts, dependency versions, project manifest, lockfile, runtime actor / supervisor / FSM counts, doctor summary, the last few log lines -- and it trusts the person in front of the terminal to ignore the rest. That is what John did, and that is what john is.

Three names, one answer

You can ask for the report anywhere you have Cure at hand:

$ mix cure.john
$ cure john
cure(1)> :john

All three call sites funnel into a single collector and a single renderer. They accept the same switches: --width, --ansi / --no-ansi, --banner / --no-banner, --root PATH. The REPL form inherits the session's theme and colour state, so :john looks like the rest of your session, not like an escaped visitor from another one.

What is in the report

The report is divided into deliberately unapologetic sections:

Cure. Version, escript entry point, whether :cure is loaded and started, the count of Cure.Std.* modules that are currently in memory, the pipeline event-bus size, the protocol registry size, and a UTC timestamp.

BEAM / OTP. Elixir / OTP / ERTS versions; the full system banner with the jit / smp / ds flags left in; scheduler topology split into online, total, dirty CPU, and dirty I/O; logical processor count; process, port, and atom counts each paired with their limit; ETS table count; a seven-way memory breakdown (total, processes, binary, code, ETS, atom, system); reductions; runtime; wall-clock uptime; and internal / external wordsize.

System. OS family and version, architecture, hostname, user, home, cwd, and selected environment variables (LANG, TERM, SHELL, PATH entry count).

Tooling. Resolved paths of z3, git, make, and cc, plus loaded versions of every declared dependency. If you have ever sent somebody into a debugging session only to learn three minutes later that they did not actually have Z3 installed, this section is for you.

Project. When Cure.toml is present in the chosen root: name, version, source paths, lockfile status, and the full dependency table. When it is absent, the section just says so; there is no tantrum.

Runtime. A condensed Cure.Observe.Top snapshot. If you are running an actor system, cure top already gives you the full view; john gives you the counters and the first five supervisors at a glance.

Doctor. The severity counters from Cure.Doctor.run/1. The full report still lives behind cure doctor; this section just tells you how loud it would be if you ran it.

Recent logs. Tails of up to five log files under .cure/logs/*.log, _build/cure/logs/*, or an erl_crash.dump at the project root, sorted by mtime. If something went sideways two minutes ago, the evidence is right there.

CommonMark all the way down

Cure.John.render/2 produces CommonMark. Cure.John.run/1 then pushes it through one of two renderers depending on what the runtime can actually load:

defp render_markdown(md, opts) do
  case Keyword.get(opts, :ansi, :auto) do
    false ->
      md

    _ ->
      try_marcli(md, opts) || Markdown.render(md, fallback_theme(opts))
  end
end

Marcli.render/2 gives us the rich Markdown-to-ANSI pipeline we already use for cure doc. It depends on MDEx, a Rust NIF, which cannot be loaded from inside the bundled cure escript archive. Every call site that might run from the escript therefore needs to fall back, so run/1 automatically drops to Cure.REPL.Markdown.render/2 -- the pure-Elixir block-aware renderer introduced in v0.29.0 -- when Marcli cannot load. The output looks slightly less polished in the fallback path, but it stays readable, and it keeps cure john identical to mix cure.john in terms of information, which is the point.

Passing --no-ansi short-circuits both renderers and emits the raw Markdown. That is what you want to feed into CI logs, jq, or any parser that prefers a document to a screenshot.

A map that is safe to serialise

Cure.John.collect/1 is the boring part of the feature and therefore the part I am most fond of. It returns a plain Elixir map: no PIDs, no references, no anonymous functions. Every value in the tree is either a primitive, a list of primitives, or a nested map of the same. That lets tests assert on it, lets you drop it into a structured logger, and lets you serialise it to JSON if the operational context calls for it.

snapshot = Cure.John.collect(root: "/path/to/project")
snapshot.vm.schedulers_online
# => 16
snapshot.project.lockfile
# => "missing"

Cure.John.render/2 takes that map and returns a Markdown string. Cure.John.run/1 does both and writes the result; it also returns the rendered string for callers that want to keep a copy.

Name tributes

I know that naming a feature after a colleague is mostly a vanity. I am doing it anyway, and I would do it again.

John, if you ever read this: thank you. The terminal knows where it is now.

What's next

The long-horizon items carry over unchanged:

  • Monomorphisation. Specialise polymorphic functions whose call sites all use concrete types. Deferred from v0.24.0.
  • Profile-guided optimisation. Feed Cure.Profiler data into the inliner and pattern-aware SMT encoder so hot paths get specialisation and cold paths stay small.
  • Time-travel for actors. v0.28.0 added @record + cure replay for FSMs; the actor surface still needs journal hooks and a deterministic scheduler.
  • First-class Cure notebook format (.cnb). Literate programming in Cure with type-checked code cells. The doc pipeline from v0.29.0 already covers most of what it would need.
  • WASM target. Compile the Cure compiler via AtomVM so the Playground's type checker and sandbox can run entirely in the browser without a WebSocket round-trip.

The repository lives at github.com/am-kantox/cure-lang. The new on-disk reference is docs/JOHN.md. The full CHANGELOG lists every touch point.