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 cellskept := 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 7The 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 entries7.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 disposeOne 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 endsThe 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
Puttook 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
owningparameter (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 programinside a choice, a loop, aselector a failure handler of an initialization block is refused (5799): move it to the top level and put the condition inside its statement. A bareleaveabove a unit finalizer is refused (5801): move the finalizer above theleave. - A generic routine’s
disposeof 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_openand__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.