Take a two-field record and one innocent line:
b := a;Now answer, in the language you use most: if a.x changes tomorrow, does
b.x change with it?
In most mainstream languages the answer is it depends on what a is — a
number no, an object yes, and for anything in between you had better know
which kind the library author chose. Whole categories of bugs live in that
table: the list two functions accidentally share, the “copy” that wasn’t, the
mutation that arrives from a module you never imported.
Mica’s answer is no. Not “no for scalars” — no. An assignment copies what
you see, two names never share storage unless one of them says pointer, and
you cannot write the sharing without writing the word.
The example is
examples/ValueAndPointer.
Build and run it:
make -C examples/ValueAndPointer runAssignment copies the value
a.x := 3;
a.y := 4;
b := a;
a.x := 99;
WriteLn(" a.x is now %lld, and b.x is still %lld", a.x, b.x); a.x is now 99, and b.x is still 3b := a copied both fields, and from that moment the records are strangers.
There is no shared backing object behind them, because there is no backing
object at all — a record is its bytes, laid out where the variable lives.
Arrays follow the same rule:
t := s;
s[2] := -1; s[2] is now -1, and t[2] is still 20Notice the two things that did not happen: the array did not decay to a pointer, and the assignment did not alias two names to one array object. One rule for every type, which is why the proof fits in two lines.
A parameter is a value
The same rule decides what a call can do to you:
procedure Grow(v : Point);
begin
v.x := v.x + 100;
WriteLn(" inside Grow, the copy's x: %lld", v.x);
end;Grow(a);
WriteLn(" a.x after Grow returned: %lld", a.x); inside Grow, the copy's x: 199
a.x after Grow returned: 99v is Grow’s own record — born as a copy at the call, dead at the return. The
mutation inside is real and the caller never sees it. Passing a value can never
be the moment two routines start sharing state, which is why you can read a
call like Grow(a) and know, from the call site alone, that a is the same
after it as before.
That guarantee is doing quiet work all over the language. It is why
a task’s arguments are snapshots: schedule Worker(cell)
copies like every other call, and no scheduling delay can turn the copy into a
window. Concurrency did not need a new rule — it inherited this one.
The pointer, written down
Sharing storage is sometimes exactly what you want. Mica does not forbid it; it makes you write it:
var
n : int64;
p : pointer int64;n := 7;
p := address n;
value p := value p + 10; n after 'value p := value p + 10': 17Three spellings carry the whole mechanism. pointer int64 declares the arrow’s
type. address n — the only way an arrow comes to exist — takes one. And
value p is the cell behind it, on the reading side and the writing side
alike.
The dereference is never implicit on a scalar. That sounds like ceremony until
the day you audit a function: every line that can touch shared storage
contains the word value, so the search box answers a question that
elsewhere requires a data-flow analysis.
A pointer is a value too
What does the one assignment rule say about pointers themselves?
q := p;
value q := 50;
WriteLn(" n through p, after a write through q: %lld", value p); n through p, after a write through q: 50q := p copies the pointer — the arrow, not the target. Now one cell stands
behind two arrows, and a write through either is visible through the other.
This is real sharing, and it is exactly as wide as the two pointer
declarations that admit it. WYSIWYG: what you see marked is what is shared,
nothing else is.
The explicit out-parameter
Put pointer in a parameter and you have the honest version of the pattern
every language needs — a routine that changes its caller’s variable:
procedure Bump(cell : pointer int64);
begin
value cell := value cell + 1;
end;Bump(address n); n after Bump(address n): 51Both ends of the agreement are written down. The declaration says pointer, so
the callee admits what it does; the call site says address, so a reader
scanning the caller sees, on that line, that n can change there. This is the
idiom the standard library’s ReadLn and Val families use for their results
— which is why a conversion that fills your variable is always spelled
Val("1234", address count) and never hides the write.
Record fields reach through a pointer
One convenience, precisely bounded:
pp := address a;
pp.y := 41; a.y after 'pp.y := 41': 41
pp.x reads the pointee's x: 99A field selection through a record pointer needs no value — pp.y is
unambiguous, so the language does not make you decorate it. The value
spelling belongs to scalars, where a bare p would be ambiguous between the
arrow and the cell.
What this does not do
A pointer to a local cannot outlive the local. Try to smuggle one out:
function Leak() : pointer int64;
var
local : int64;
begin
local := 5;
Leak := address local;
end;analyzer error 5150: cannot return pointer to local variable 'local': only
heap-origin pointers, pointer parameters, or lexical parent-scope variables
are allowedThe frame dies at the return; an arrow into it would dangle, so the program is
refused. The full story of which pointers may travel how far — owning
pointers, borrows, and the alias form — is the ownership article of this
series, coming with the memory section.
Copies cost what they cost. A 24-byte record copies as 24 bytes; a large array assignment is a large copy, visible in the source exactly where it happens. Mica will not secretly share to save you the copy — if the copy is too expensive, the pointer is the tool, and it is spelled.
No pointer arithmetic. An arrow points at the cell it was taken from. Walking memory is what arrays are for, with bounds the compiler can reason about.
address is not free of rules. Inside a concurrent block, taking a bare
address of shared state is refused outright — the arrow would outlive the
lock that guards each marked touch. That refusal and its legal replacement are
in data-race freedom.
What the compiler proved
Every copy in this program was visible, every share was spelled, and the one
attempt to make an arrow outlive its cell did not compile. You can now read
any Mica assignment, call, or address and answer the question this article
opened with — from the line itself, not from a table of type kinds.
Next
Records and arrays in full — the shapes values come in, nesting, and what passing and returning whole aggregates costs and guarantees. (In preparation; its example is next in the repository.)