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 itReplace 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 ownInside 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/UsesGaugeIt prints:
doubled 42Where 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 16A 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 nodesA 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 timestream 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_writelnand 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
forloop whose index a nested routine assigns (5782), whose address is taken (5783), or which a select arm or anon failhandler 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 oneaddress xsand 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 int64is refused (5787): declare the parameterstream of int64 from process. A loop over such a stream answers the loss withon lostor, in a generator whose own stream saysfrom process, passes it on (5786), and anon lostsection 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-contractadds 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_storeand__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.