Mica 7.7.7 follows ownership through the shapes programs keep their cells in: dynamic arrays and records stored whole, copies read out of a field at any depth, lists handed by value, appends into another owner’s array, variables a nested routine fills, and streams pulled after a view of their generator ended. A program that frees a cell twice or reads it after its release on any of these roads is refused where it is written, with a message that names the way out, and the checked build stops the two roads it still let through. So a program has two outcomes: the compiler refuses it and says what to write instead, or it compiles, and a checked build stops whatever the compiler could not follow.

A map owns the cells of its entries, and a generic routine releases a value of its type parameter that it owns. Subrange and enumeration values live in registers, and optimized code comes closer to GCC -O2’s again, from literal arguments built at the call to logical immediates and float literals read where they are used. A library’s generic types import whole, nil passes to a generic routine, one syntax mistake draws one error, and an initialization block runs exactly the finalizers it reaches.

A copy stays its owner’s

{
    WholeArrays - a dynamic array stored whole hands its cells over, and a copy read out of a field stays the field's.

    'items := other' makes 'items' the owner of the cells 'other' held, after it released its own, and 'other' keeps
    their addresses as a borrowed copy. 'kept' is read out of the holder's field and reads the cells the field owns; the
    holder's release gives them back once.
}
program CopyReleased;

imp
    WriteLn : std;

const
    WeightLine = "weight %lld";
    Capacity = 1;
    FirstWeight = 7;
    SecondWeight = 9;

type
    { A cell the arrays below own. }
    Node = record
        weight : int64;
    end;

    { An array whose elements own their cells. }
    Nodes = array of pointer Node;

    { A record whose array owns its cells. }
    Holder = record
        items : Nodes;
    end;

{ A fresh array of one element owning a fresh cell of the given weight. }
function Filled(weight : int64) : Nodes;
var
    cell : pointer Node;
begin
    Filled := new Nodes reserve Capacity;
    cell := new Node;
    cell.weight := weight;
    Filled[0] := cell;
end;

{ The store whole hands the cells over, and the copy out of the field reads what the field owns. }
procedure Demonstrate();
var
    items : Nodes;
    other : Nodes;
    kept : Nodes;
    holder : Holder;
begin
    items := Filled(FirstWeight);
    other := Filled(SecondWeight);
    items := other;
    WriteLn(WeightLine, items[0].weight);
    WriteLn(WeightLine, other[0].weight);
    dispose items;

    holder.items := Filled(FirstWeight);
    kept := holder.items;
    WriteLn(WeightLine, kept[0].weight);
    dispose kept;
    dispose holder;
end;

begin
    Demonstrate();
end.

7.7.7 refuses the second release where it stands:

obligation error 13109 [62,5]: heap cell is released by hand although the container holding it releases it: 'kept' holds a copy of another array's value, made at line 60 on a path reaching here, and a copy holds the addresses of the cells, never the cells: they stay with the place the copy was made from, whose owner releases them, so this release would release them a second time. Leave their release to that owner, and let 'kept' only read the cells

kept := holder.items copies the array, and Mica copies values: kept gets elements of its own that hold the addresses the field holds. The cells stay the field’s, so the release of kept and then of holder would free them twice. 7.7.6 compiled this program, and its run stopped at the second release. Remove dispose kept; and the program compiles and runs:

{
    WholeArrays - a dynamic array stored whole hands its cells over, and a copy read out of a field stays the field's.

    'items := other' makes 'items' the owner of the cells 'other' held, after it released its own, and 'other' keeps
    their addresses as a borrowed copy. 'kept' is read out of the holder's field and reads the cells the field owns; the
    holder's release gives them back once.
}
program WholeArrays;

imp
    WriteLn : std;

const
    WeightLine = "weight %lld";
    Capacity = 1;
    FirstWeight = 7;
    SecondWeight = 9;

type
    { A cell the arrays below own. }
    Node = record
        weight : int64;
    end;

    { An array whose elements own their cells. }
    Nodes = array of pointer Node;

    { A record whose array owns its cells. }
    Holder = record
        items : Nodes;
    end;

{ A fresh array of one element owning a fresh cell of the given weight. }
function Filled(weight : int64) : Nodes;
var
    cell : pointer Node;
begin
    Filled := new Nodes reserve Capacity;
    cell := new Node;
    cell.weight := weight;
    Filled[0] := cell;
end;

{ The store whole hands the cells over, and the copy out of the field reads what the field owns. }
procedure Demonstrate();
var
    items : Nodes;
    other : Nodes;
    kept : Nodes;
    holder : Holder;
begin
    items := Filled(FirstWeight);
    other := Filled(SecondWeight);
    items := other;
    WriteLn(WeightLine, items[0].weight);
    WriteLn(WeightLine, other[0].weight);
    dispose items;

    holder.items := Filled(FirstWeight);
    kept := holder.items;
    WriteLn(WeightLine, kept[0].weight);
    dispose holder;
end;

begin
    Demonstrate();
end.
weight 9
weight 9
weight 7

The same rule holds wherever a whole value moves. items := other hands the cells to items, whose old value is released first, and leaves other a borrowed copy that reads them. An array or a record stored whole into a field hands its cells over once. A copy read out of a place at any depth reads the place’s cells until a store through the same path clears the place, and then it owns them. A dynamic array copied whole from one record’s field into another’s, and a store whose value lies inside the value it replaces, are refused by name.

A routine handed a list by value releases none of the cells its copy names and hands none to an owning parameter (5797, 5798), and the list stays what the caller handed in while the routine writes the caller’s place through a pointer, a second name, a global or a pointer a C routine hands back. An append of a record into a dynamic array that another owner holds gives that array the record’s cells, so a release or a read of them after the append is refused. A list reversed in place compiles.

Maps own their entries

A map’s Put takes its value over: an owning pointer, or a record whose fields own cells. A later Put on the same key, a Remove and a dispose of the map release what the entry held, and a growth moves the entries without releasing any of them.

{
    MapOwns - a map of owning pointers owns every cell it is given.

    'Put' takes the cell over, so the map releases it when a later 'Put' replaces the entry, when 'Remove' takes the key
    out and when the map is disposed. Every cell is released once, and the program releases none of them by hand.
}
program MapOwns;

imp
    WriteLn : std;
    Map, Put, Get, Remove, Count : maps;

const
    Entries = 4;
    Weight = 10;
    ReplacedKey : int64 = 2;
    ReplacedWeight = 99;
    RemovedKey : int64 = 1;
    WeightLine = "key %lld weight %lld";
    CountLine = "%lld entries";

type
    { A cell of the heap, holding one weight. }
    Node = record
        weight : int64;
    end;

    { The owning pointer to a node. }
    NodeRef = pointer Node;

{ The map takes every cell over from its 'Put' on. }
procedure Fill();
var
    m : Map of (int64, NodeRef);
    n : NodeRef;
    i : int64;
begin
    i := 0;

    while i < Entries do begin
        n := new Node;
        n.weight := i * Weight;
        Put(address m, i, n);
        i := i + 1;
    end;

    { the replacing 'Put' releases the cell the entry held }
    n := new Node;
    n.weight := ReplacedWeight;
    Put(address m, ReplacedKey, n);
    WriteLn(WeightLine, ReplacedKey, Get(m, ReplacedKey).weight);

    { the removal releases the removed entry's cell }
    Remove(address m, RemovedKey);
    WriteLn(CountLine, Count(m) as int64);
    dispose m;
end;

begin
    Fill();
end.
key 2 weight 99
3 entries

7.7.6 refused this program, since its map kept only copies of the pointers it was given and every cell stayed the program’s to release. In 7.7.7 the map releases them, and a program that releases a cell after Put took it is refused at the release:

obligation error 13104 [51,5]: heap cell is disposed again after it was already released on every path reaching here — the call to 'Put' at line 50 took the cell allocated at line 48 through an owning parameter, which hands its release to the callee; remove this dispose

One cell handed to two entries and a variable’s own address handed to Put are refused by name too. The maps unit can do this because a generic routine may now release a value of its type parameter where the value is its own, a variable of its frame or a parameter declared owning V. Each specialization releases what its type argument owns: a pointer’s cell, a record’s owning fields, and nothing for a number or a string. A release of any other such value, a borrowed V parameter among them, is refused (5800).

Views end where their cells go

A stream’s generator runs at the pulls of its stream, not at the call that makes it, so a view the generator reads must still stand at every pull. A stream pulled after its generator’s view ended is now refused at compile time, whether a loop, a select, a stage or a task pulls it, and a stream closed while its generator waits judges the generator’s cleanup at the close.

A view a call answers over an array’s elements, a copy read out of a heap record and an array stored into a heap record’s field end when their cells are released, through any pointer to the record and through a nested routine that may release it. A read after that is refused (5790). A dynamic array cannot grow while a window that outlives its routine may borrow it (5794, 5795), nor while a view of it is lent to a call that may grow it (5796), since the growth moves the elements out from under the view.

The checked build stops every road

A checked build traps what the compiler cannot follow, and 7.7.7 closes the two roads it still let through. A checked run stops with use_after_dispose where a runtime routine such as Append reaches into a released cell through an address the program handed it, and where a pointer that also holds cells of its own is used through an address inside a released cell. Only a checked program that can hand a runtime routine such an address links the map that answers the question, so every other checked program links what it linked before.

Initialization blocks run the finalizers they reach

A defer until program in a library’s or an object’s initialization block is a unit finalizer: it runs once when the program ends. It ran whether or not its place in the block ever ran, so one inside an if that was not taken still ran. 7.7.7 keeps the promise every defer makes, that a path which never reached it does not run it, by asking the finalizer to stand at the top level of the block:

{
    journal - a library whose initialization block opens its log and closes it when the program ends.

    The finalizer stands inside the 'if' that opens the log, so it would run when the program ends whether or not the log
    was opened. The compiler refuses it there.
}
library journal;

imp
    WriteLn : std;

var
    lines : int64;

{ Counts one line of the log. }
exp procedure Note();
begin
    lines := lines + 1;
end;

begin
    lines := 0;

    if lines = 0 then begin
        WriteLn("log opened");
        defer until program WriteLn("log closed");
    end;
end.
analyzer error 5799 [26,9]: a 'defer until program' in an initialization block is a unit finalizer, which runs once when the program ends whether or not its place ran, so it must stand at the top level of the block, but found it inside one 'if statement' - move it to the top level of the initialization block and put the condition inside its statement, or, where the cleanup belongs to the conditional work, do that work in a routine whose own 'defer' runs when the routine ends

The finalizers a program runs are then the ones written, in the order written. A bare leave ends an initialization block as it ends a routine’s body, where it stopped the compiler, and a leave that would skip a finalizer below it is refused by name (5801).

Code closer to GCC -O2

At O1 and O2 a value of a subrange, an enumeration or an error domain lives in a register like the integer it is stored as, where it lived in a frame slot that was stored and loaded around every call. A bitwise and, or or xor with a literal takes the literal as the instruction’s immediate on both processors, as GCC does, instead of building it in a register first in every round of a loop.

A constant a call receives is built in its argument register at the call, with no frame slot, and a frame store nothing reads goes even where a jump lands on it. On x86-64 a float literal is read from the constant pool by the instruction that uses it, after a call and in a loop that calls, and a parameter that waits across a call is stored straight from the register it arrived in. On AArch64 a remainder by a literal in a loop keeps its divisor in a register, a product that feeds an addition or a subtraction is one madd or msub, a factor a loop multiplies by is built once ahead of the loop, and a parameter’s first read takes the register it arrived in. A value through a full or an empty stream ring costs two instructions less.

Generic types across libraries

A program specializes every generic type a library exports, records holding a dynamic or a fixed array of the type parameter among them, and the library’s generic routines take those specializations. A specialization over a type a library exports is one type wherever it is named, so Count(m) over a Map of (int64, NodeRef) whose NodeRef points at an imported record compiles, and an exported record’s array bounds may name the library’s own constants.

A nil handed to a generic routine takes the pointer type the call’s other arguments bind, as it does in a call of an ordinary routine, so Put(address m, 4, nil); compiles over a map of pointers.

One mistake, one message

After a syntax error the parser reports nothing more until it reaches the next statement or declaration, so one mistake draws one error, and that error stands at the first token that breaks the construct: array[4 of int32; draws “expected range separator ‘..’, found of” at the of. An operation refused for its operand types draws no second error where its value is read. A refused generic call quotes its argument as the program wrote it, and a record that holds itself through an array is refused with the pointer that works written out, Forest = array of pointer Tree (5793).

One place, one verdict

Every spelling of a place, h.next.form, value (h.next).form or p := h.next; p.form, reaches the ownership checks as one place, so the three spellings draw one verdict, and the field chain and its dereference spelling compile to the same code. The ownership analysis takes time in proportion to the routine it reads, so a routine of a thousand allocations compiles in a fraction of a second.

In the editor

The extension’s export overview draws the road an export took through the compiler: one box per component in the order the compiler runs them, and under each box the files it writes, each a link that opens it. A tab shows the other road, the debug road or the release road that O1 and O2 take. Export File and Profile File stand in a Mica source’s context menu inside a Dev Container too, the Mica Developer container among them.

Upgrading

Things a 7.7.6 build meets differently in 7.7.7:

  • A copy of a dynamic array read out of a field, a parameter or any place is a borrowed copy, and releasing it is refused (13109): leave the release to the place’s owner, or clear the place through the same path so the copy owns the cells. A writable address of a borrowed copy is refused where it is taken (13116).
  • A cell used or released after an append put it into another owner’s array is refused (13104, 13105): move the use before the append. A cell released after a map’s Put took it is refused (13104): leave its release to the map.
  • A routine handed a value by value cannot release a cell read out of it or hand one to an owning parameter (5797, 5798): leave the release to the caller, or take the value through a pointer.
  • A dynamic array cannot grow, or be handed to a writable pointer parameter, while a window that outlives its routine may borrow it (5794, 5795), and a view of it cannot be lent to a call that may grow it (5796): form the window in the routine that reads it, below the growth.
  • A defer until program inside a choice, a loop, a select or a failure handler of an initialization block is refused (5799): move it to the top level and put the condition inside its statement. A bare leave above a unit finalizer is refused (5801): move the finalizer above the leave.
  • A generic routine’s dispose of a value of its type parameter that it does not own is refused (5800).
  • Dragon SDK: the runtime floor contract gains the checked twins of the array, matrix and string-buffer routines, __mica_descriptors_release_cell, __mica_descriptors_give_back_tail_checked, __mica_loan_window_open and __mica_loan_window_close, and moves to version 9, so a floor built for 7.7.6 is refused at the link by the version it states. 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.