Upgrading from 0.4.x to 0.5.0
0.5.0 has breaking changes. Most programs compile unchanged, but the type checker now rejects several shapes that used to typecheck and then crash at run time, a few standard-library APIs changed their signatures, and hot-reload servers need one restart after the upgrade.
Read § 1 first if you run anything with --hot-reload or forge deploy hot.
Everything else is fixed at compile time: the compiler tells you where, and
usually how.
The full list of changes, including everything that is new, is in
CHANGELOG.md.
1. Hot-reload servers need one cold restart
The hot-reload ABI is now march-hcr-v4. A server built by 0.4.x cannot take a
hot patch built by 0.5.0. The first forge deploy hot after you upgrade the
toolchain is refused before upload, with a message telling you to restart the
server once on a base build from the new toolchain. Nothing is deployed, and the
server stays up.
What to do: rebuild each hot-reload server with 0.5.0 and restart it once. Hot deploys work again from then on. A patch built by 0.4.x sent to a 0.5.0 server is refused with “rebuild the patch”.
2. forge deploy hot refuses two more kinds of deploy
A patch that changes a parameter’s borrow mode. Whether a String, List or
ADT parameter is borrowed or owned is decided by the function’s body, and running
callers depend on it. A body edit that flipped it used to deploy and then free a
value a running caller still used. Such a deploy is now refused, naming the
parameter (parameter 1 of Handler.count changed from borrowed to owned). Deploy
that build with a restart. Body-only changes that keep the modes deploy hot as
before.
A topology pool whose derived capabilities widen. When topology.toml leaves a
pool’s caps out, the compiler derives them, and a deploy that gives the pool a
capability it did not have at the environment’s last completed deploy now stops
before anything is built. Either name the new capability:
forge deploy hot --env prod --grant-cap IO.FileWrite
or write the pool’s caps in topology.toml, which takes the pool out of the
check. forge build warns about each pool that widens.
3. The type checker rejects more programs
Every item here used to typecheck and then fail at run time, or was a warning.
Calling or passing a function at the wrong arity (arity_mismatch,
callback_arity_mismatch). March has no partial application, so a two-argument
function applied one argument at a time crashed. This is now caught for a function
passed as a callback, through a let h = add alias, and for a callback passed to a
let-bound lambda. Call the function with all its arguments, or pass the curried
wrapper the hint suggests:
-- before: typechecked, then crashed
Map.insert(m, k, v, lt) -- lt : (a, a) -> Bool
-- after
Map.insert(m, k, v, fn a -> fn b -> lt(a, b))
Signature type variables are rigid (annotated_tyvar_fixed). A body that fixes
a signature’s a to a concrete type, or makes two signature variables the same
type, is an error. It was a warning since 0.4. Write the concrete type the hint
names, or make the body generic.
Linearity holes are closed. These are now errors:
- discarding a linear value with
let _ = …or a_pattern; - consuming a linear value on only some branches of an
iformatch; - capturing a linear value in a closure or a local
fn; - consuming a record’s linear field and keeping the record;
- a parameter that holds a
lineartype variable without being marked; - a module-level
letof a linear type.
A module-level let annotation is checked. let x : Int = "hello" directly
inside a mod used to be accepted.
A path scope that cannot take effect is an error. Only IO.FileRead,
IO.FileWrite and IO.FileSystem take a scope, and it must be absolute:
needs IO("/srv") becomes needs IO.FileSystem("/srv").
4. Standard-library changes
RingBuf is linear. Every operation consumes the buffer and hands it back:
let rb = RingBuf.push(rb, x)
let (n, rb) = RingBuf.size(rb)
In actor state, write { state with buf: RingBuf.push(state.buf, x) }. A buffer
can now be sent in a message, which moves it.
Config reads through typed keys.
-- before
Config.put(:myapp, :port, 4000)
Config.get(:myapp, :port)
-- after
let port = Config.key(:myapp, :port, Config.int())
Config.put(port, 4000)
Config.get(port) -- Option(Int)
The same change applies to put_in/get_in (use Config.key_in), to
store_put/store_get, and to from_env*, validate, get_with_default and
require.
A running cluster node is a capability. ClusterNode.start(io, cfg) takes the
root capability first and returns Result(Cap(ClusterNode.Live), String). The
ClusterHandle type is gone. Start the node in main(io : Cap(IO)), write
Cap(ClusterNode.Live) where you wrote ClusterNode.ClusterHandle, and add
needs ClusterNode.Live to a module whose signatures name it.
Finding an actor you were not handed takes a capability. Actor.whereis,
registered, list, top_by_mailbox and over_mailbox take a
Cap(Actor.Introspect) first. Mint it once with let c = Actor.introspect(io).
pid_of_int is replaced by Actor.pid_from_int(c, n).
Contract preconditions moved into signatures. Stats.median, std_dev, iqr,
iqr_default and quantile_default take a non-empty list; Seq.batched and
Flow.batch take a positive size; Gen.frequency takes a non-empty list. A
provably bad argument is a compile error, and a function that passes its own
unrefined argument gets a warning. march --check --refine-suggest <fn> file.march
prints the refinement to add to your function, or guard the call with a match.
A program that cannot be executed is an Err. Process.run, run_stream,
spawn_async and System.cmd return Err("cannot execute …") for a missing or
non-executable program. Compiled code used to return Ok with exit code 127, so
match Err instead. Children now start with default signal handling, and on macOS
they inherit only stdin, stdout and stderr.
Programs that use the cluster transport directly (NetKernel, ClusterConn,
NodeSend, NodeQueue, …) under a narrow grant need IO.Mut and IO.Clock.
Choreography API.
run_<Role>and its fronts returnResult(Session.Outcome, RunError). Code that matchesOk(_)is unaffected.- A hosted role’s
canceltakes the session:cancel(s, parked). - A session frame refused with
DELIVERY_FAILEDfails its session, and no longer reachesClusterNode.on_delivery_failed.
Corrected signatures. get_actor_field returns Option(Int); sha256 is
Bytes -> String; Plot.save returns Result(Unit, File.FileError); the
Compress functions return Compress.Error; RRB and AhoCorasick use
Array.PVec(a).
5. Flags and tooling
--trmc,--no-trmcandMARCH_NO_TRMCare gone; tail-recursion-modulo-cons always runs. Therespondbuiltin is gone.forge.tomlis parsed strictly: a malformed line is an error that names its line, and an unknown key is a warning.- Compiled
int_shl/int_shrwith a shift count outside[0, 62]panic, and interpretedint_shris an arithmetic shift.
Known issues in 0.5.0
- Each hot deploy keeps one patch
.somapped until the node restarts. - A hot deploy that changes a boundary function’s types can still reach a caller that is already running old code. Restart for signature changes.
- Two nested types with the same short name (
A.Note,B.Note) are one type to the checker. Give them distinct names. let k = List.lengththenk(xs)runs interpreted but does not compile. Wrap it:fn xs -> List.length(xs).
Quick checklist
- Rebuild and restart every hot-reload server once on 0.5.0.
- Run
march --check(orforge check) and fix each new error; the hints name the fix. - Search for
Config.put(/Config.get(,ClusterNode.start(,Actor.whereis,pid_of_intandRingBuf.and migrate them as in § 4. - Replace any exit-code-127 check on
Process.*with a match onErr. - Remove
--trmc/--no-trmcfrom build scripts.