Migrating to v0.1.6¶
What a module imports no longer reaches the files that import it¶
A module's own bare imports used to come along with it: importing a.cdl
also brought in the names a.cdl merged from its imports, and a module bound
with as exposed the modules it bound as nested namespaces (a::json::parse).
Now a file sees what a module declares and nothing it imports. A name that
arrived this way is a compile error, unknown_function or
unknown_namespace.
This affects you if a program calls a function or names a type that one of its modules imports rather than declares.
Before, with shapes.cdl holding import "std/math"; and its own functions:
After, importing the module it needs itself:
std/assert and std/random drop the module name from their functions¶
Now that an import binds the module's name, the functions that repeated it are renamed:
| Before | After |
|---|---|
assert(cond) |
assert::that(cond) |
assert_msg(cond, message) |
assert::msg(cond, message) |
assert_true(cond) |
assert::is_true(cond) |
assert_false(cond) |
assert::is_false(cond) |
assert_eq(a, b) |
assert::eq(a, b) |
assert_ne(a, b) |
assert::ne(a, b) |
random::random_int() |
random::int() |
random::random_int_range(min, max) |
random::int_range(min, max) |
random::random() |
random::float() |
random::random_range(min, max) |
random::float_range(min, max) |
The failure messages of eq and ne start assert::eq failed: and
assert::ne failed: instead of assert_eq failed: and assert_ne failed:.
Nothing else changes. Calling an old name is a compile error,
unknown_function or unknown_function_in_namespace, whose help names the new
one.
This affects you if a program or a test calls one of the old names, or matches on the old failure message.
Before:
After:
A host or dylib block's functions are called with :: only¶
A function a host or dylib block declares used to be reachable two ways,
app::rows(id) and app.rows(id). The dot form is now a compile error,
block_dot_call, whose help names the :: call. A block is a namespace, and
:: is how every namespace is reached; . is for the fields and methods of a
value.
This affects you if a script calls a block's function with a dot. A variable that shares the block's name is unaffected: a dot on it still calls the value's own method.
Before:
After:
Rebuild your .cdlb artifacts: the artifact format is now version 15¶
The .cdlb format moved from version 14 to version 15. Version 15 added the
instruction the is type test compiles to for structs, enums, unions and
collections, and the nested type codes that check what a list or a map holds.
This affects you if you ship or keep .cdlb files built by an earlier
release. The new candela-vm, and load_program in an embedding host, refuse
them:
Rebuild each artifact from its source with the new toolchain:
An embedding host that builds artifacts with build_bytecode needs no code
change; rebuild the artifacts it wrote before the upgrade.
Rebuild your .cdlb artifacts again: the artifact format is now version 16¶
The .cdlb format moved to version 16. It records the entry the runtime runs
event handlers through for a program that imports std/event, and the
instructions std/event and the new std/fs functions compile to.
This affects you if you ship or keep .cdlb files built by an earlier
toolchain, including one that already wrote version 15. The new candela-vm,
and load_program in an embedding host, refuse them:
Rebuild each artifact from its source with the new toolchain:
An embedding host that builds artifacts with build_bytecode needs no code
change. One that fills in a ProgramImage field by field sets the new
event_entry field, None for a program with no event handlers.
std/convert and its to_* methods are removed¶
The to_int, to_float, to_bool and to_string methods spelled the
conversions a second way. The conversions are the type names called as
functions: int(x), float(x), bool(x) and string(x). The module is gone
from the prelude and from the library directory, so import "std/convert"; no
longer resolves, and calling one of the methods is a compile error,
no_such_method, whose help names the conversion.
This affects you if a program calls one of the four methods or imports
std/convert.
Before:
After:
Default::default() is removed; a struct literal ends in a bare .. instead¶
Default::default() is gone in every position it used to resolve in: after
.. in a struct literal, as an argument to a struct-typed parameter, in a
return, and as the value of a struct-typed field. A literal that ends in a
bare .. takes every field it does not write from the struct's default, and
S::default() builds the whole default as before. Calling
Default::default() is a compile error, unknown_function, whose help names
S { .. } and S::default().
This affects you if a program writes Default::default().
Before:
struct Options {
cwd: string = ".",
verbose: bool,
}
fn run(opts: Options) -> string {
return opts.cwd;
}
fn main() {
let o = Options { verbose: true, ..Default::default() };
print(run(Default::default()));
}
After:
struct Options {
cwd: string = ".",
verbose: bool,
}
fn run(opts: Options) -> string {
return opts.cwd;
}
fn main() {
let o = Options { verbose: true, .. };
print(run(Options::default()));
}
A list of ints or floats passed to a C function sees what C writes into it¶
A list passed to a dylib function used to be a copy C could write into
without the program ever seeing it. A list of ints or of floats is now copied
back once the call returns, so whatever the function wrote into the buffer is
in the list afterwards. Lists of strings, lists of lists and structs are still
copies.
This affects you if a program passes a list of numbers to a C function that writes into the buffer it is handed, and expects the list to be unchanged afterwards. Pass a copy instead.
Before, xs was unchanged after the call:
dylib "mylib" {
sort_in_place(int[], int);
}
fn main() {
let xs = [3, 1, 2];
mylib::sort_in_place(xs, xs.len());
print(xs);
}
After, it holds what the function wrote: [1,2,3]. To keep the original, pass
a copy:
dylib "mylib" {
sort_in_place(int[], int);
}
fn main() {
let xs = [3, 1, 2];
let scratch = xs[0..];
mylib::sort_in_place(scratch, scratch.len());
print(xs);
}
find is split: find(pred) searches with a predicate, index_of(x) finds a position¶
xs.find(x) and s.find(needle) were the index search and answered -1 when
nothing matched; xs.find(f) with a function was the list module's predicate
search and answered null. Each operation now has one name, and neither has a
sentinel:
xs.index_of(x)ands.index_of(needle)answerSomeof the position, orNone.xs.find(f)answersSomeof the first elementfaccepts, orNone.
find on a list with an argument that is not a function is a compile error,
find_takes_a_function, whose help names index_of. find on a string is
no_such_method, whose help names index_of.
This affects you if a program calls find, compares a position with -1, or
calls the list module's index_of, which also answered -1.
Before:
fn main() {
let xs = [10, 20, 30];
let i = xs.find(20);
if i != -1 {
print(i);
}
print("hello".find("l"));
print(xs.find(fn(x) => x > 15));
}
After:
fn main() {
let xs = [10, 20, 30];
if xs.index_of(20) is Some(i) {
print(i);
}
print("hello".index_of("l").unwrap_or(-1));
print(xs.find(fn(x) => x > 15).unwrap());
}
fs raises fs_directory_not_empty and fs_other for failures it had no kind for¶
A file system failure whose kind the fs_ error list did not name reached code
the runtime assumed could not run, so what followed was undefined: the program
could stop, or carry on in a broken state, and no catch could take it. The most common one was
fs::delete_dir on a directory that still has entries. Those failures now
raise catchable errors: fs_directory_not_empty for that case, and fs_other
for any kind the list still does not name.
This affects you if a program catches the fs_ errors by kind and relied on
one of these failures never being raised. Catch the new kinds where you catch
the others.
Before, this was undefined:
import "std/fs";
fn main() {
fs::mkdir("full");
fs::write("full/a.txt", "a");
fs::delete_dir("full");
}
After, it raises an error you can catch:
import "std/fs";
fn main() {
fs::mkdir("full");
fs::write("full/a.txt", "a");
try {
fs::delete_dir("full");
} catch "fs_directory_not_empty" {
print("delete the files first");
}
}
The file functions are the std/fs module¶
fs::read, fs::write and the other file functions used to be a built-in
namespace that needed no import. They are now the std/fs module, so a program
that uses them imports it. The functions, their arguments and the errors they
raise are unchanged. A call with no import is a compile error,
unknown_namespace, whose help names the import.
This affects you if a program calls any fs:: function.
Before:
After:
A default function in a struct's impl block is now its default¶
A default declared in a struct's impl block used to have no effect:
S::default() and every other default built the struct from its field
values. It now replaces that default, and a default that does not fit the
new role is a compile error.
This affects you if an impl block declares a function named default.
A fn default() -> S with no parameters now runs wherever a default of the
struct is built: S::default(), a literal ending in a bare .., and a field
whose type is the struct. If it builds something other than what
the field values give, those call sites now see its value.
A default with parameters, including a method fn default(self), or one that
returns another type, no longer compiles:
Rename the method, or make it the struct's default:
An import binds the module's name instead of merging its names into scope¶
import "std/json"; used to merge every function, struct and enum the module
declares into the importing file's scope. It now binds the module under the
last segment of its path, so its names are reached through it:
json::parse(s). as still picks another name, so import "std/json" as
json; can drop the as. To write some names with no prefix, list them:
import "std/assert" { eq, ne }; brings in those items, and a type named this
way brings the functions its impl blocks declare. There is no form that
brings in everything.
A call to a name the file now reaches only through the module's name is a
compile error, unknown_function, whose help names both ways to write it. A
type written bare is unknown_struct or unknown_namespace with the same
help. A path whose last segment is not a name, such as "./my-lib.cdl", needs
as; without it the import is import_name_not_identifier.
This affects you if a program imports a module without as and calls or names
what it declares with no prefix, or writes a generic type such as Set<int>
from a module it imported with as.
Before:
import "std/assert";
import "std/json" as json;
fn main() {
assert_eq(json::stringify([1, 2]), "[1,2]");
}
After:
Or, naming the items:
import "std/assert" { eq };
import "std/json" { stringify };
fn main() {
eq(stringify([1, 2]), "[1,2]");
}
The is_* and as_* built-ins are replaced by is and as¶
Testing and downcasting a dynamic value are operators now. v is T replaces
the seven is_int, is_float, is_str, is_bool, is_list, is_map and
is_null functions, and v as T replaces the six as_int, as_float,
as_str, as_bool, as_list and as_map functions. T is any type an
annotation names, so the operators also test structs, enums, unions and what a
list or a map holds. Inside an if or a while whose condition is v is T,
v already has the type T, so a test followed by a downcast becomes the test
alone. is is a keyword now.
Calling an old function is a compile error, unknown_function, whose help
names the replacement.
This affects you if a program calls any of the thirteen functions, or names a
variable or a function is.
| Before | After |
|---|---|
is_int(v), is_float(v), is_bool(v) |
v is int, v is float, v is bool |
is_str(v) |
v is string |
is_list(v), is_map(v) |
v is any[], v is {any: any} |
is_null(v) |
v == null |
as_int(v), as_float(v), as_bool(v) |
v as int, v as float, v as bool |
as_str(v) |
v as string |
as_list(v), as_map(v) |
v as any[], v as {any: any} |
is and as bind like <: looser than arithmetic, so as_int(v) + 1 is
v as int + 1, and x + as_int(v) needs parentheses, x + (v as int).
Before:
import "std/json" as json;
fn main() {
let doc = as_map(json::parse("{\"n\": 7}"));
if is_int(doc["n"]) {
print(as_int(doc["n"]) + 1);
}
}
After:
import "std/json";
fn main() {
let doc = json::parse("{\"n\": 7}") as {string: any};
let n = doc["n"];
if n is int {
print(n + 1);
}
}
The json_parse and json_stringify built-ins are removed¶
The two built-in functions are gone. Parsing and serialising json is
json::parse and json::stringify from std/json, which behave exactly as the
built-ins did. Calling either old name is a compile error, unknown_function,
whose help names the module function and its import.
This affects you if a program calls json_parse or json_stringify.
Before:
After:
first, last, min and max on a list return an Option¶
xs.first(), xs.last(), xs.min() and xs.max() used to hand back the
element and raise index_out_of_bounds on an empty list. They now return
Some of the element, or None for an empty list, and never raise.
This affects you if a program uses one of them as the element itself.
Before:
After:
In a function that returns an Option itself, xs.first()? reads the element
and hands a None back to the caller.
list.partition(sep) is renamed list.split(sep)¶
Cutting a list at each separator element is now split, the name a string
uses for the same operation; the behaviour is unchanged. Calling partition is
a compile error, no_such_method, whose help names xs.split(separator).
This affects you if a program calls partition on a list.
Before:
After:
m.get_or(k, fallback) is removed¶
The get_or method of std/map is gone, now that m.get(k) returns an
Option. Calling it is a compile error, no_such_method, whose help names the
replacement, m.get(k).unwrap_or(fallback).
This affects you if a program calls get_or on a map. A program that imported
std/map only for get_or can drop the import too: the map methods are part of
the prelude.
Before:
After:
m.get(k) returns an Option instead of the value¶
m.get(k) used to hand back the value stored under k and raise
unknown_map_key when the key was absent. It now returns Some of the value,
or None when the map holds no such key, and never raises. Reading a value
that has to be there is m[k], which raises unknown_map_key the way get
did.
This affects you if a program calls get on a map and uses the result as the
value.
Before:
After, when the key has to be there:
Or, when it may be absent:
Map entries are written with m[k] = v; insert is removed¶
m.insert(k, v) is gone. A map entry is written with brackets, m[k] = v,
which adds the entry or replaces its value, and m[k] += 1 and the other
compound assignments work on an entry. Calling insert is a compile error,
no_such_method, whose help names m[key] = value.
This affects you if a program calls insert on a map.
Before:
fn main() {
let counts = {};
counts.insert("a", 1);
counts.insert("a", counts.get("a") + 1);
print(counts);
}
After:
print(null) prints null¶
Printing null, the value of a call that returns nothing, used to write
nothing at all, not even a line break. It now writes null on its own line,
under candela and candela-vm alike.
This affects you if a program prints a value that can be null and relies on
the line being left out.
Before, this printed one line, done; it now prints done and then null:
Print only the values you mean to show:
A set is made with set::Set<int>::new(); the module function new is removed¶
std/set no longer declares a module-level new<T>(). The constructor is a
function of the Set<T> type, called by its path: set::Set<int>::new(), or
Set<int>::new() when the import names the type with
import "std/set" { Set };. A function
an impl block declares with no parameters is now called this way for every
type, so Point::origin() works the same. Calling set::new is a compile
error, unknown_function_in_namespace, whose help names
set::Set<T>::new().
This affects you if a program makes a set.
Before:
After:
str(x) is renamed string(x)¶
The conversion to text takes the name of the type it produces, like int(x),
float(x) and bool(x). Calling str is a compile error, unknown_function,
whose help names string(x).
This affects you if a program calls str. A program that declared its own
function named string now shadows the conversion with it.
Before:
After:
s.is_int() and s.is_float() are replaced by s.parse<T>()¶
The two string methods answered whether the text held a number, and the number
was then read with a second call. s.parse<T>() does both: it answers
Some(value) when the text holds an int, a float or a bool, and None
otherwise. Calling is_int or is_float on a string is a compile error,
no_such_method, whose help names parse.
"42".parse<float>() is Some(42.0), where "42".is_float() was false: a
float reads from integer text too.
This affects you if a program calls is_int or is_float on a string.
Before:
fn port(text: string) -> int {
if text.is_int() {
return int(text);
}
return 8080;
}
fn main() {
print(port("80"), port("x"));
}
After:
fn port(text: string) -> int {
if text.parse<int>() is Some(n) {
return n;
}
return 8080;
}
fn main() {
print(port("80"), port("x"));
}
throw is a statement: throw "kind";¶
throw used to be written as a call, throw("kind"). It is now a statement
and a keyword, written like return: throw "kind";. The kind is any string
expression, as before. The call spelling is a parse error, throw_call, that
names the statement form.
This affects you if a program raises its own errors with throw, or uses
throw as the name of a variable or a function.
Before:
After:
The string trim methods are renamed to trim_start, trim_end and trim_chars¶
The trims name the end of the string they work on as start and end, which holds for right-to-left text, and the ones that strip a set of characters say so:
| Before | After |
|---|---|
s.trim_left() |
s.trim_start() |
s.trim_right() |
s.trim_end() |
s.trim_sequence(chars) |
s.trim_chars(chars) |
s.trim_sequence_left(chars) |
s.trim_start_chars(chars) |
s.trim_sequence_right(chars) |
s.trim_end_chars(chars) |
s.trim() is unchanged. The behaviour of each is unchanged. Calling an old
name is a compile error, no_such_method, whose help names the new one.
This affects you if a program calls one of the old names.
Before:
After: