← All posts

Cure v0.14.0: Real-World Readiness

by Aleksei Matiushkin

release package-management type-holes testing interop

v0.12 built the body. v0.13 gave it teeth. v0.14 makes it usable.

The theme is real-world readiness. A language is real-world ready when programs can have dependencies, the type system follows values through function calls, and the editor tells you what types it inferred before you even ask. v0.14 delivers all three.

What changed

Six phases, each adding a capability the language never had.

1. Package management

Every Cure program was a single file or a manually orchestrated collection. Not anymore. Cure now has Cure.toml project files:

[project]
name = "my_app"
version = "0.1.0"

[dependencies]
utils = { path = "../shared/utils" }

[compiler]
type_check = true
optimize = true

cure init my_project scaffolds a new project with Cure.toml, lib/, and test/ directories. cure deps resolves dependencies – path-based for now, with git support planned. Dependencies are compiled before the main project, their .beam files added to the code path automatically.

The standard library is treated as an implicit dependency. cure stdlib compiles it once; projects reference the compiled output.

2. Deep dependent type tracking

v0.13 verified guard constraints at call sites with literal arguments. v0.14 extends this to track value knowledge through the program:

Path-sensitive refinement. In if x > 0 then safe_head(v) else default, the then-branch knows x > 0 and the else-branch knows not (x > 0). The type checker creates refinement types per branch via refine_env_from_condition, extending the guard-based refinement from multi-clause functions (v0.13) to if/else and match arms.

SMT context accumulation. The checker maintains a stack of known constraints as it descends into expressions. At each call site, all accumulated constraints are asserted before checking the function’s precondition.

3. LSP type holes

The single most impactful developer experience feature. Write _ in any type annotation position and the LSP tells you what goes there:

fn foo(x: _) -> _ = x + 1

The LSP immediately shows: parameter x is Int, return type is Int. The code action “Fill type hole with Int” does the rest.

Type holes are parsed as {:type_hole, :infer} in the AST. During type checking, when a hole is encountered, the checker infers the type that would make the program well-typed and emits a :type_hole_inferred pipeline event. The LSP reports these as hint diagnostics (severity 4) and offers fill-in code actions.

4. LSP incremental compilation

The LSP now caches parsed ASTs per document URI and version number. When didChange fires, if the version hasn’t changed, the reparse is skipped entirely. This eliminates redundant work during rapid typing and reduces Z3 invocations.

5. Cross-module protocol dispatch

v0.13 built the global protocol registry. v0.14 uses it for actual cross-module function calls.

When an unqualified call doesn’t match local functions, imports, or variables, the codegen now checks the protocol registry. If a matching protocol method exists, it emits a remote call to the registered module’s dispatch function. If the registry has no entry at compile time (module not yet compiled), a runtime lookup via ProtocolRegistry.lookup_impl plus apply is generated as fallback.

6. Testing infrastructure

Cure programs can now be tested in Cure:

mod Std.Test
  fn assert(condition: Bool) -> Atom
  fn assert_eq(a: T, b: T) -> Atom
  fn assert_ne(a: T, b: T) -> Atom
  fn assert_gt(a: T, b: T) -> Atom
  fn assert_lt(a: T, b: T) -> Atom

cure test compiles and runs .cure test files, reporting pass/fail counts. Std.Test is the 18th stdlib module.

Elixir interop

Cure modules compile to BEAM modules with a Cure. prefix – mod Calculator becomes :"Cure.Calculator". Calling from Elixir is direct:

defmodule CureExample do
  def factorial(n), do: :"Cure.Calculator".factorial(n)
  def greet(name), do: :"Cure.Greeter".hello(name)
end

The examples/cure_example/ directory is a full Mix project that depends on Cure, compiles .cure files as a build step, and calls them from Elixir with doctests proving the interop works.

The website

Cure now has a documentation website built with Phoenix, NimblePublisher, and makeup_cure for syntax highlighting. Nord color theme (dark/light). Eight content pages covering the language guide, type system, FSMs, protocols, standard library, tooling, and roadmap. This blog.

The numbers

606 tests. Zero credo issues. Zero compilation warnings. 49 Elixir source files. 18 stdlib modules. ~210 exported functions. 11 example programs plus the interop project.

995 lines of new code across 13 files in this release cycle.

The Cure rewrite is functionally complete

After v0.14, the only unported legacy Erlang code is:

  • cure_type_optimizer.erl (7,196 lines) – bulk monomorphization and profile-guided optimization, largely superseded by the function inliner
  • cure_runtime.erl (970 lines) – bytecode interpreter, not needed since Cure compiles directly to BEAM
  • Native helpers (697 lines) – replaced by stdlib + FFI

The rewrite is done.

Getting started

git clone https://github.com/Oeditus/cure.git
cd cure
mix deps.get && mix test
mix escript.build
./cure init my_project
./cure version          # Cure 0.14.0

The repository is at github.com/Oeditus/cure.