Skip to content

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:

import "./shapes.cdl" as shapes;

fn main() {
    print(shapes::math::sqrt(16.0));
}

After, importing the module it needs itself:

import "std/math";
import "./shapes.cdl";

fn main() {
    print(math::sqrt(16.0));
}

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:

import "std/assert";

fn main() {
    assert_true(1 < 2);
    assert_eq(2 + 2, 4);
}

After:

import "std/assert";

fn main() {
    assert::is_true(1 < 2);
    assert::eq(2 + 2, 4);
}

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:

host "app" {
    int rows(string);
}

fn count(id: string) -> int {
    return app.rows(id);
}

After:

host "app" {
    int rows(string);
}

fn count(id: string) -> int {
    return app::rows(id);
}

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:

candela-vm: unsupported .cdlb format version 14 (this runtime understands version 15)

Rebuild each artifact from its source with the new toolchain:

candela build game.cdl

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:

candela-vm: unsupported .cdlb format version 15 (this runtime understands version 16)

Rebuild each artifact from its source with the new toolchain:

candela build game.cdl

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:

fn main() {
    print("42".to_int() + 1);
    print(3.9.to_int());
    print(2.5.to_string() + "!");
}

After:

fn main() {
    print(int("42") + 1);
    print(int(3.9));
    print(string(2.5) + "!");
}

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) and s.index_of(needle) answer Some of the position, or None.
  • xs.find(f) answers Some of the first element f accepts, or None.

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:

fn main() {
    fs::write("notes.txt", "hello");
    print(fs::read("notes.txt"));
}

After:

import "std/fs";

fn main() {
    fs::write("notes.txt", "hello");
    print(fs::read("notes.txt"));
}

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:

impl Config {
    fn default(self) -> Config { // struct_default_signature
        return self;
    }
}

Rename the method, or make it the struct's default:

impl Config {
    fn reset(self) -> Config {
        return self;
    }
}

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:

import "std/assert";
import "std/json";

fn main() {
    assert::eq(json::stringify([1, 2]), "[1,2]");
}

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:

fn main() {
    let doc = json_parse("{\"n\": 7}");
    print(json_stringify(doc));
}

After:

import "std/json";

fn main() {
    let doc = json::parse("{\"n\": 7}");
    print(json::stringify(doc));
}

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:

fn main() {
    let xs = [3, 1, 2];
    print(xs.max() + xs.first());
}

After:

fn main() {
    let xs = [3, 1, 2];
    print(xs.max().unwrap_or(0) + xs.first().unwrap_or(0));
}

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:

fn main() {
    print([1, 0, 2, 0, 3].partition(0));
}

After:

fn main() {
    print([1, 0, 2, 0, 3].split(0));
}

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:

import "std/map";

fn main() {
    let counts = {"a": 1};
    print(counts.get_or("z", 0));
}

After:

fn main() {
    let counts = {"a": 1};
    print(counts.get("z").unwrap_or(0));
}

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:

fn main() {
    let ages = {"ada": 36};
    print(ages.get("ada") + 1);
}

After, when the key has to be there:

fn main() {
    let ages = {"ada": 36};
    print(ages["ada"] + 1);
}

Or, when it may be absent:

fn main() {
    let ages = {"ada": 36};
    print(ages.get("alan").unwrap_or(0) + 1);
}

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:

fn main() {
    let counts = {};
    counts["a"] = 1;
    counts["a"] += 1;
    print(counts);
}

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:

fn log(message) {
    print(message);
}

fn main() {
    print(log("done"));
}

Print only the values you mean to show:

fn log(message) {
    print(message);
}

fn main() {
    log("done");
}

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:

import "std/set" as set;

fn main() {
    let s = set::new<int>();
    s.add(1);
    print(s.len());
}

After:

import "std/set";

fn main() {
    let s = set::Set<int>::new();
    s.add(1);
    print(s.len());
}

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:

fn main() {
    print("count: " + str(3));
}

After:

fn main() {
    print("count: " + string(3));
}

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:

fn check(n: int) {
    if n > 2 {
        throw("too_big");
    }
}

After:

fn check(n: int) {
    if n > 2 {
        throw "too_big";
    }
}

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:

fn main() {
    print("  hi  ".trim_left() + "|");
    print("--hi--".trim_sequence("-"));
}

After:

fn main() {
    print("  hi  ".trim_start() + "|");
    print("--hi--".trim_chars("-"));
}