Mica 7.7.6 gives borrowed pointers their general rules. A view of a cell may be kept only where the cell’s owner outlives the place that keeps it, any change of the owner’s place ends the view, and a call may not reach one place through two arguments when either of them can write it. A read of freed memory that only a checked build used to stop is now refused where the program writes it.

A library is one file: its archive and its shared object carry its contract, so a program imports it by naming the binary, and it exports sum types, trees of records and owning parameters that its importers use as their own. A call names its arguments with a colon, a sum type’s case takes its values by name, and a sum type may hold an array of itself, so a value like a JSON document is one type. A stream that may come from a worker process says so in its type, and a lost worker is answered wherever that stream travels. Optimized code comes closer to GCC -O2’s on both processors, the arrays unit sorts records by a key, stably where a program asks, and the editor finds every reference and every caller.

A borrowed pointer follows its owner

{
    EditorViewEnded - a read through a borrowed pointer after a change of its owner's place, which the build refuses and
    the editor shows at the same place.

    The view is copied out of the holder's field, and the holder is lent to 'Replace', whose heading takes it in a
    writable parameter, so the call can release the cell the view points at. The read after the call is refused with
    error 5790, and the editor names the same code, line and column as the build on open, on save and after a pause in
    typing.
}
program EditorViewEnded;

imp
    WriteLn : std;

const
    Line = "weight %lld";
    FirstWeight = 7;
    NextWeight = 9;

type
    { A node is the cell a holder owns. }
    Node = record
        weight : int64;
    end;

    { A holder owns the one cell its field names. }
    Holder = record
        node : pointer Node;
    end;

{ The holder's cell is released and a new one takes its place. }
procedure Replace(h : pointer Holder);
var
    old : pointer Node;
begin
    old := h.node;
    h.node := nil;
    dispose old;
    h.node := new Node;
    h.node.weight := NextWeight;
end;

{ The view is copied out of the holder's field before the holder is lent to 'Replace'. }
procedure Weigh();
var
    holder : Holder;
    seen : borrowed pointer Node;
begin
    holder.node := new Node;
    holder.node.weight := FirstWeight;
    seen := holder.node;
    Replace(address holder);
    WriteLn(Line, seen.weight);
    dispose holder.node;
end;

begin
    Weigh();
end.

7.7.6 refuses the read where it stands:

analyzer error 5790 [53,23]: 'seen' is read through after its view ended at the call to 'Replace' at line 52, whose parameter 'h' can change 'holder', since any change of its owner's place ends a borrowed pointer and the cell it pointed at may be released there: read through 'seen' before that change, or take the view again after it

Replace takes the holder through a pointer parameter, so its heading says it can change the holder, and the cell seen points at may be gone after the call. The compiler refuses such a read where it is written, and names the call that ended the view.

A view may be kept only where its owner outlives the place: in the routine that took it, in its locals and the heap cells they own, in a place reached through the owner itself, or anywhere when the owner is a global. A view of a routine’s own cell kept in a global, in an enclosing routine’s variable or in a heap record another owner holds is refused at the store (5788), and so is a store whose declarations cannot order the owner and the place (5789).

Any change of the owner’s place ends its views: a store into it, a dispose, a hand-over to an owning parameter, a call whose heading can write it, a call of a routine nested in its scope and, for a place reached from a global, any call. Each of these is read off a routine’s heading, never off its body, so a callee can change its body without changing what its callers may do. A read through an ended view is refused with the change and its line named (5790). A value that carries a kept view cannot leave its owner’s reach (5791), and a cell may be released once every holder of its kept view has ended (5792 while one may still read).

The same rules let more programs through. A routine may answer a holder that views the cell it was lent, held := Build(node);, and the caller may release the holder and then the node. A node lent into a dynamic array’s elements, two pointers deep or afresh in every round of a loop is released once its holders have ended. These programs were refused before.

One place, one writer per call

A routine function Twice(p : pointer Cell, q : pointer Cell) : int64; reads and writes the cell through both of its parameters. A program that hands it one cell twice, total := Twice(c, c);, compiled before; 7.7.6 refuses the call:

analyzer error 5784 [41,23]: the call to 'Twice' reaches 'c' through parameter 'p', which can write it, and through parameter 'q', which can write it, and a place one parameter can write reaches the routine through that parameter alone: hand 'c' to one of the two, and give the other a place of its own

Inside a routine two parameter names are two places, so the compiler judges Twice as though p and q never meet. A call that reaches one place through two arguments, either of which can write it, breaks that promise, and it is refused with the place and both parameters named (5784). Values are copies taken before the call and never count. Two elements of one array count as two places when their indices are two different constants; otherwise the call is refused (5785), and the way out is to hand the routine one address xs and the two indices. A cell handed to an owning parameter leaves its slot before the routine runs.

A library is one file

Every library link writes the library’s contract into its archive and its shared object. A program imports the library by naming the binary, in contracts in its mica.project or with --external-contract, and the mica.external file beside it becomes optional. Here is a library of one routine:

{
    The library 'gauge' is built with no contract file beside its archive, so the archive alone is what a program names.
}
library gauge;

{ The sample doubled, the one routine the library exports. }
exp function Doubled(sample : int64) : int64;
begin
    Doubled := sample * 2;
end;

begin
end.

and a program that imports it:

{
    UsesGauge - a program imports a library by naming its archive, and no contract file.

    The archive of 'gauge' carries its contract inside it, so the archive alone is what the program names.
}
program UsesGauge;

imp
    WriteLn : std;
    Doubled : gauge;

var
    answer : int64;

begin
    answer := Doubled(21);
    WriteLn("doubled %lld", answer);
end.

The library is built from a mica.project that states its version, and the program names the archive itself:

mica --compile --link --output-modifier archive --optimize O2 \
  --platform linux,arm64,utf-32 --project mica.project \
  --source gauge.mica --build build
mica --compile --link --optimize O2 --platform linux,arm64,utf-32 \
  --external-contract build/gauge.a --source UsesGauge.mica --build app
./app/UsesGauge

It prints:

doubled 42

Where a binary and a mica.external beside it both stand, they state the same contract or the import is refused naming both files. A binary that carries no contract, such as a C archive or a library an earlier compiler built, is read through the mica.external beside it. mica --inspect on a library prints the contract it carries as the member contract, and the contracts reference describes the road.

A library now exports sum types. It marks one exp, exp Label = (LabelMissing, LabelText(text : string, weight : int32));, and its importer builds, matches, stores and releases the values as its own; the library’s C header writes the type in the tagged form C programs use. A library exports a tree of records whose children are pointers to the record, and an importer resolves the cycle as one unit does. A routine’s owning parameter takes its cell over in an importer too, so a release after the call is refused where it freed the cell twice. A library’s C header defines MICA_ENCODING_UTF8 or MICA_ENCODING_UTF32, so a C host includes it with no flag of its own.

Arguments by name

{
    NamedArguments - a call names the parameters it gives values to, in any order, and leaves out any defaulted one.

    A call may write a parameter's name and a colon before a value, behind the values it gives by position. 'Clip' is
    called with its middle default left out while the last parameter is given, 'Pair' with its arguments named in
    reverse, and 'Increase' with the pointer it writes through named after the step.
}
program NamedArguments;

imp
    WriteLn : std;

const
    ClipFormat = "clip %.2lf";
    PairFormat = "pair a %lld b %lld";
    CountFormat = "count %lld";

var
    counter : int64;

{ The clip of one value keeps it between two bounds, the unit interval unless the call names others. }
function Clip(x : float64, low : float64 := 0.0, high : float64 := 1.0) : float64;
begin
    Clip := x;

    if x < low then
        Clip := low;

    if x > high then
        Clip := high;
end;

{ A pair is printed as its two values, whichever order the call names them in. }
procedure Pair(a : int64, b : int64);
begin
    WriteLn(PairFormat, a, b);
end;

{ A counter grows through the pointer it is handed, by one unless the call names another step. }
procedure Increase(total : pointer int64, step : int64 := 1);
begin
    value total := value total + step;
end;

begin
    WriteLn(ClipFormat, Clip(3.0, high: 2.0));
    WriteLn(ClipFormat, Clip(high: 0.5, x: 0.75, low: 0.25));
    WriteLn(ClipFormat, Clip(x: 0.1, low: 0.2));

    Pair(b: 2, a: 1);
    Pair(1, b: 2);

    counter := 10;
    Increase(step: 5, total: address counter);
    Increase(address counter);
    WriteLn(CountFormat, counter);
end.

It prints, at the debug tier and at O2, on both processors:

clip 2.00
clip 0.50
clip 0.20
pair a 1 b 2
pair a 1 b 2
count 16

A call names an argument with a colon, as a record value names a field. Values by position come first, each parameter takes one value, the values are evaluated in the order written, and a parameter the call leaves out takes its default wherever it stands. That holds for nested, generic and elemental routines, for the standard library’s, Pos among them, and for C routines through the names their contracts state. Parameter names are part of a library’s contract.

A parameter without a default may now stand behind one with a default, function Pick(a : int64 := 1, b : int64) : int64;, which Pick(b: 2) calls. A sum type’s case takes its values by its fields’ names, ShapeBox(depth: 4, width: 2, height: 3). The arrow => is refused with the colon to write (1049), and a misnamed, repeated or missing argument is refused by the parameter’s name.

Values that hold values of their own kind

{
    JsonValues - a case of a sum type holds a dynamic array of the sum type itself, so a value nests inside values of
    its own kind as deep as the program builds it.

    A list is built, copied and then replaced while its copy keeps every number, and a tree four levels deep is copied
    and then emptied while its copy keeps every node.
}
program JsonValues;

imp
    WriteLn, Append, Length : std;

const
    LeafText = "leaf";

type
    Json = (JsonNull, JsonNumber(value : int64), JsonText(text : string), JsonList(items : array of Json),
            JsonObject(members : array of JsonMember));

    JsonMember = record
        name : string;
        item : Json;
    end;

var
    doc, copy, tree : Json;

{ A list of the numbers one to 'count'. }
function Numbers(count : int64) : Json;
var
    items : array of Json;
    k : int64;
begin
    for k := 1 to count do
        Append(address items, JsonNumber(k));

    Numbers := JsonList(items);
end;

{ The sum of every number a value holds at any depth, the members of an object included. }
function Total(doc : Json) : int64;
var
    k, sum : int64;
begin
    sum := 0;

    case doc of
        JsonNull, JsonText(*): sum := 0;
        JsonNumber(n): sum := n.value;
        JsonList(l):
            for k := 0 to Length(l.items) - 1 do
                sum := sum + Total(l.items[k]);
        JsonObject(o):
            for k := 0 to Length(o.members) - 1 do
                sum := sum + Total(o.members[k].item)
    end;

    Total := sum;
end;

{ The count of values a value holds at any depth, itself among them. }
function Nodes(doc : Json) : int64;
var
    k, count : int64;
begin
    count := 1;

    case doc of
        JsonNull, JsonNumber(*), JsonText(*): count := 1;
        JsonList(l):
            for k := 0 to Length(l.items) - 1 do
                count := count + Nodes(l.items[k]);
        JsonObject(o):
            for k := 0 to Length(o.members) - 1 do
                count := count + Nodes(o.members[k].item)
    end;

    Nodes := count;
end;

{ A tree 'depth' levels deep whose every list holds 'width' children, each leaf a number or a text in turn. }
function Nest(depth, width : int64) : Json;
var
    items : array of Json;
    k : int64;
begin
    if depth = 0 then
    begin
        if width mod 2 = 0 then
            Nest := JsonText(LeafText)
        else
            Nest := JsonNumber(width);
    end
    else begin
        for k := 1 to width do
            Append(address items, Nest(depth - 1, k));

        Nest := JsonList(items);
    end;
end;

begin
    doc := Numbers(5);
    WriteLn("built %lld in %lld nodes", Total(doc), Nodes(doc));

    copy := doc;
    doc := Numbers(2);
    WriteLn("copy kept %lld, source now %lld", Total(copy), Total(doc));

    tree := Nest(4, 3);
    copy := tree;
    tree := JsonNull;
    WriteLn("deep copy %lld in %lld nodes, source %lld nodes", Total(copy), Nodes(copy), Nodes(tree));
end.

It prints, at the debug tier and at O2, on both processors:

built 15 in 6 nodes
copy kept 15, source now 3
deep copy 13 in 35 nodes, source 1 nodes

A sum type’s case may hold a dynamic array of the sum type itself, JsonList(items : array of Json), of a record that holds it, as JsonObject does through JsonMember, or of a second sum type whose case holds the first. Such values are built, matched, copied, passed, returned, grown and replaced like every other sum value, give back everything they hold, and reach a task or a worker process as a copy of their own. The copy above keeps every node of the tree after its source is emptied.

A worker’s stream says so in its type

{
    WorkerRing - a lost worker's stream ends lost through a generator that feeds a ring.

    A generator whose parameter and stream say 'from process' runs as a task that feeds a ring. Its loop passes a lost
    worker on, so the loop over the ring runs its 'on lost' section. A worker that ends cleanly sends every value through
    the ring, and no section runs. The doomed producer kills its own worker before it emits a value, so its section runs
    once and its loop reads nothing.
}
program WorkerRing;

imp
    WriteLn : std;
    Kill, GetPid, SIGKILL : posix;

const
    ValueCount = 10;
    RingSlots = 4;
    ScheduledLine = "scheduled: %lld values, sum %lld, the loss answered %lld time";

{ This producer emits the numbers from one to n in a worker process, and kills its own worker first when it is doomed. }
generator Wide(doomed : bool, n : int64) : stream of int64;
var
    k : int64;
    killed : int32;
begin
    if doomed then
        killed := Kill(GetPid(), SIGKILL) on fail use -1;

    for k := 1 to n do
        emit k;
end;

{ This adapter doubles every value and passes a loss on to the loop that reads its stream. }
generator Doubled(s : stream of int64 from process) : stream of int64 from process;
var
    x : int64;
begin
    for x in s do
        emit x + x;
end;

{ This procedure reads the ring the adapter feeds as its scheduled producer. }
procedure Scheduled(doomed : bool);
var
    s : stream of int64;
    ring : stream of int64;
    y : int64;
    count : int64;
    sum : int64;
    losses : int64;
begin
    s := schedule Wide(doomed, ValueCount) on process;
    ring := schedule Doubled(s) buffer RingSlots;

    for y in ring do
    begin
        count := count + 1;
        sum := sum + y;
    end
    on lost do
        losses := losses + 1;

    WriteLn(ScheduledLine, count, sum, losses);
end;

begin
    Scheduled(False);
    Scheduled(True);
end.

It prints, at the debug tier and at O2, on both processors:

scheduled: 10 values, sum 110, the loss answered 0 time
scheduled: 0 values, sum 0, the loss answered 1 time

stream of int64 from process names a stream a worker process may feed, in a type, a parameter and a generator’s heading. A loop over it answers a lost worker with on lost or, in a generator whose own stream says so, passes the loss on, as Doubled does, so the loop that reads the last stream of a chain of adapters runs its on lost section. A loop or a select answers a lost worker only where a worker’s stream can reach it.

A producer on a worker sends each value as it makes it, so a lost worker no longer takes every value it had not yet sent. Leaving a concurrent block ends a stage’s wait on a pull or a select at once, its defers run and its producer is cancelled or its worker ended. A program whose only placements are on worker processes may touch its globals in a concurrent block without synchronized, since a worker holds none of the program’s memory.

Code closer to GCC -O2

{
    FrameArrayScalarReplacementFolded - A local array filled and read at fixed places leaves no trace in memory.

    Every access to 'scores' names one element at a literal index and the array's address never leaves the routine, so the
    optimizing tiers give each element a variable of its own. The stored scores then reach the loads that read them, the
    sums fold, and the program prints a total the compiler computed: the routine stores nothing into its frame and loads
    nothing back, where it used to store three elements and read five.
}
program FrameArrayScalarReplacementFolded;

imp
    WriteLn : std;

const
    TotalLine = "total = %d";
    FirstScore = 10;
    SecondScore = 20;

type
    { Three scores at the places one to three. }
    Trio = array[1..3] of int32;

{ The scores are filled and summed at fixed places, so the total is known at compile time. }
procedure ShowTotal();
var
    scores : Trio;
    total : int32;
begin
    scores[1] := FirstScore;
    scores[2] := SecondScore;
    scores[3] := scores[1] + scores[2];
    total := scores[1] + scores[2] + scores[3];
    WriteLn(TotalLine, total);
end;

begin
    ShowTotal();
end.

At O1 and O2 the three scores live in registers, the sums fold, and ShowTotal hands the writer the total it computed at compile time, with no store into its frame and no load from it. On x86-64:

  mov         esi, 60
  lea         rdi, qword ptr [rip+.Ll2.84]
  mov         al, 0
  call        __stdio_utf32_writeln

and on AArch64 the same total is one movz w1, #60.

At the same tiers an integer division or remainder by a literal multiplies by the divisor’s reciprocal, as GCC does, every quotient and remainder the one the divider gives. An AArch64 prologue saves its frame record and its callee-saved registers in pairs with stp and restores them with ldp, and every unwinder still finds each saved register. A store that nothing reads goes, a nil guard the program’s own test already decided goes, and a routine inlined at every call keeps no dead copy. Optimized builds start every function, and on AArch64 every loop head, on GCC -O2’s boundaries, so unrelated code no longer moves a loop’s time. An O2,profiling build’s code is the release build’s to the byte again.

Under UTF-8, a read of s[i] in any while, repeat or counted loop walks from the nearest place it already knows instead of the string’s first byte, at every tier, so a walk over a long string takes time in proportion to its length, where it grew with the square of the length.

Records sort by a key

The arrays unit sorts any array by a key array the caller fills, one key per element at the same position. SortByKeys(address people, address ages); sorts in place with no allocation, and StableSortByKeys(address people, address ages); keeps people of equal age in the order they had, so a sort by name followed by a sort by age leaves every age group ordered by name. StableSort(address numbers); is the stable twin of Sort.

One mistake, one message, and the editor

A refused declaration still declares its name, so its uses, and the operators, casts and generic calls built on them, draw no second error. A refused part of a routine’s heading leaves the rest standing, so its fails clause still holds, and a name declared twice belongs to the declaration the source writes first, whatever the two kinds. Every message names a place by its line and column, and a program that stops opens its report with the reason alone:

Mica runtime failure: reason=index_out_of_range (12)

The context line after it still names the file, the line and the column, and the source line follows.

The language server answers Find All References, Peek References and Show Call Hierarchy: every place a variable, a field, a type or a routine is named, across every unit of the project and through its imports, and for a routine its callers and its callees with the place of each call. A generic routine’s references include every specialization’s calls, and a routine a C contract declares is found in every unit that imports it.

A checked run stops with use_after_dispose when a program reads or writes through an address taken inside a heap cell after the cell was released: a field’s, an element’s, or one inside the cell’s own dynamic array. Remark 13111 names the two roads it still does not stop.

The language described gains its chapters on statements, routines, failures, memory, tensors and the tape and the C boundary, each with programs the website’s checks compile, and the traps reference explains every reason a program can stop with.

Upgrading

Things a 7.7.5 build meets differently in 7.7.6:

  • A counted loop’s index changes only through the loop, as ISO Pascal states. A for loop whose index a nested routine assigns (5782), whose address is taken (5783), or which a select arm or an on fail handler of its body stores into, is refused. Such loops compiled and could run forever; count the loop with a variable no nested routine assigns and whose address is never taken.
  • A borrowed pointer kept where its owner may not outlive it is refused at the store (5788, 5789): keep the view in a variable of the routine, or keep the owner itself there instead of a view of its cell. A read through a view after its owner’s place changed is refused (5790): read before that change, or take the view again after it.
  • A call that reaches one place through two arguments, either of which can write it, is refused (5784), so Twice(address holder, address holder); no longer compiles: hand the place to one of the two parameters and give the other a place of its own. Two elements of one array whose indices are not two different constants are refused (5785): hand the routine one address xs and the two indices.
  • Programs that freed a cell twice are refused where they would: after a call to a routine nested in the cell’s owner that may release it on some paths only (13114), after it was stored through a global pointer, and when it is released both through a slot behind a pointer chain and through the name that stored it. A local’s address stored through a pointer field, h.next.v := address x, is refused as a frame escape (5379).
  • A worker’s stream handed to a parameter written stream of int64 is refused (5787): declare the parameter stream of int64 from process. A loop over such a stream answers the loss with on lost or, in a generator whose own stream says from process, passes it on (5786), and an on lost section that no worker’s stream can reach is refused as one that never runs (5600).
  • Two entries of a C contract that state different routines on one linked symbol are refused as the contract loads (467): give each routine its own symbol, or state the second entry’s routine word for word as the first.
  • A flag of one value given twice with two different values is refused by name; a list flag such as --external-contract adds the second value’s paths.
  • A trap report opens with Mica runtime failure: reason=<reason> (<code>) and no longer names a token stream index.
  • Dragon SDK: the runtime floor contract moves to version 8, with the entries __mica_heap_address_check, __mica_descriptors_clone_tail, __mica_dynamic_array_clone_store and __mica_string_code_point_near_utf8. Rebuild a floor of your own against this release before you link it.

The packages and the commands that install them are on the install page.