This chapter describes how a Mica program computes a value: the operands an
expression is made of, the operators that join them and how tightly each binds,
the types each operator takes and gives, integer arithmetic in the width of its
type and what the checked build traps, the comparisons, the boolean, set and
string operators, conversions written with as, the expressions the compiler
computes while it compiles, and calls. Each construct names its rule in
the grammar, says what it means, and quotes
what the compiler answers when a source breaks the rule. The types themselves,
promotion and the conversions each pair of types admits are
chapter 4, and the capabilities that decide which
operator a type takes are the type system,
which this chapter links rather than repeats.
Every example is complete, compiled and run by the compiler’s own test suite on AMD64 and AArch64, and the output shown beneath it is the output those runs are held to.
Operands
An operand is a factor (factor): one primary, the selectors behind it, and one optional prefix before it. A primary (primary) is one of these:
| Primary | Written | Rule |
|---|---|---|
| a name | total, Budget | an identifier |
| a literal | 12, 2.5, 'A', "good", nil | literal, whose forms are chapter 1 |
| a call of a function | Length(greeting) | function-call |
| a value in parentheses | (quantity + shipping) | parenthesized-value |
| a composite value at once | (x: 1.0, y: 2.0) | parenthesized-value with a comma or a field name |
| a set | [Mon..Fri], [Sat, Sun] | set-constructor |
| an allocation | new Node | allocation, taught in heap end to end |
A selector (selector) picks a part
of the value before it: .name a field, [i] an element, [i, j] an element
by several indices and [low..high] a slice, and selectors follow one another,
orders[i].price. The prefix is a sign, + or -, or one of the words
address and value, which take a variable’s address and read through a
pointer, as value and pointer teaches. A sign
belongs to its factor, so it may follow an operator: price * -quantity is
written as it reads. value reads through the pointer before the selectors
apply, and a sign and address apply to the selected value.
The elements of a set constructor are values of the set’s element type, and
low..high stands for every value from low to high: [Tue..Thu, Sat] over
the days of the week holds four of them. Sets teaches them.
Operators and their precedence
Operators bind from the weakest to the strongest in five levels, and the
operators of one level group from the left, so total - 4 - 2 is
(total - 4) - 2:
| Level | Operators | Rule |
|---|---|---|
| weakest | or, xor | expression |
and | conjunction | |
=, #, <, <=, >, >=, in, is, and not and odd before an operand | relation, comparison-operator | |
as | cast-expression | |
+, - | simple-expression | |
| strongest | *, /, mod, shl, shr, ·, ⊙ | term, multiplying-operator |
≠, ≤ and ≥ are the same operators as #, <= and >=, and × is the
same operator as *. · is the dot product and ⊙ the elementwise product,
which vectors and matrices teaches.
Three consequences read straight from the levels. A comparison binds more
tightly than and and or, so total > 0 and total <= Budget needs no
parentheses. not negates the whole comparison behind it, so
not total > Budget is not (total > Budget), and odd tests the whole sum
behind it. And shl and shr stand with the multiplying operators, so
price + quantity shl 2 shifts quantity before the sum.
A comparison does not chain: a relation holds one comparison, so a < b < c is
not an expression, and the program writes the two comparisons it means,
a < b and b < c, which the levels already group. For a < b < c the compiler
answers at the second < with parser error 2097: “a comparison does not chain,
so ‘<’ cannot follow the comparison in front of it: a relation holds one
comparison, and the comparisons bind more tightly than ‘and’, so the two
comparisons the chain means are written ‘a < b and b < c’”.
{
Invoice - how the operators of an expression bind. The multiplying operators, 'shl' among them, bind more tightly
than the adding operators, the adding operators more tightly than 'as', 'as' more tightly than the comparisons, the
comparisons more tightly than 'and', and 'and' more tightly than 'or'. Operators of one level group from the left,
a sign belongs to the factor it stands before, and 'not' negates the whole comparison behind it.
}
program Invoice;
imp
WriteLn, Ord : std;
const
UnitPrice = 12;
Quantity = 3;
Shipping = 5;
Discount = 4;
Coupon = 2;
Budget = 50;
Places = 2;
ItemCount = 7;
GroupingLine = "%lld %lld";
LeftLine = "%lld %lld";
SignLine = "%lld %lld";
ShiftLine = "%lld %lld";
TruthLine = "%lld %lld %lld";
CastLine = "%lld";
var
price : int64;
quantity : int64;
shipping : int64;
total : int64;
items : int32;
begin
price := UnitPrice;
quantity := Quantity;
shipping := Shipping;
total := price * quantity + shipping;
{ the multiplying operators bind more tightly than the adding operators }
WriteLn(GroupingLine, price * quantity + shipping, price * (quantity + shipping));
{ operators of one level group from the left }
WriteLn(LeftLine, total - Discount - Coupon, total / quantity / Coupon);
{ a sign belongs to its factor, so it may follow an operator }
WriteLn(SignLine, -price * quantity, price * -quantity);
{ 'shl' is a multiplying operator }
WriteLn(ShiftLine, price + quantity shl Places, (price + quantity) shl Places);
{ comparisons bind more tightly than 'and', and 'and' more tightly than 'or' }
WriteLn(TruthLine, Ord(total > 0 and total <= Budget), Ord(not total > Budget),
Ord(quantity = Quantity or total > Budget and price = 0));
{ 'as' converts the whole sum before it }
items := ItemCount;
WriteLn(CastLine, items + 1 as int64);
end.It prints:
41 96
35 6
-36 -36
24 60
1 1 1
8The types of the operators
An operator takes operands of the types whose capability grants it, and two operands of different types meet in the type one of them promotes to, as chapter 4 states, where a constant without a type takes the type of the operand beside it:
| Operators | Take | Give |
|---|---|---|
+, -, *, / | two integers or two reals | the operands’ type |
mod | two integers | the operands’ type |
a sign, -x | a signed integer or a real | the operand’s type |
and, or, xor, not | two bool values, or one for not | bool |
and, or, xor, not | two integers of one type, or one for not | the bits combined in that type |
shl, shr | an integer and a shift count | the operand’s type |
odd | an integer | bool |
=, # | two values of one type that carries equality | bool |
<, <=, >, >= | two values of one ordered type | bool |
in | a member and a set of its type | bool |
+, -, * | two sets of one type | that set type |
+ | two strings | string |
The bit words and, or, xor and not take two integers of one type and
compute in that type’s own width, so the operands do not meet in a wider one.
| Written | Code | The compiler’s words |
|---|---|---|
two integers of different widths in and, a and small over an int64 and an int32 | 5117 | incompatible data types in logical operation 'and': int64 and int32 |
a truth value and an integer in and, flag and a | 5117 | incompatible data types in logical operation 'and': bool and int64 |
a real shifted, ratio shl 1 | 5117 | incompatible data types in logical operation 'shl': float64 and int32 |
a real negated with not, not ratio | 5118 | data type 'float64' cannot be used in logical operation 'not' (requires logical) |
odd of a real, odd ratio | 5114 | data type 'float64' cannot be used in unary arithmetic operation 'odd' (requires integral) |
a sign before a truth value, -flag | 5114 | data type 'bool' cannot be used in unary arithmetic operation 'negate' (requires negatable) |
a sign before an unsigned integer, -level over a uint8 | 5114 | data type 'uint8' cannot be used in unary arithmetic operation 'negate' (requires negatable) |
a string subtracted, greeting - "x" | 5113 | data type 'string' cannot be used in arithmetic operation 'subtraction' (requires numeric) |
Integer arithmetic in its width
An integer operation computes in its operands’ type and gives a value of that
type. Over two integers / drops the fraction toward zero, and mod gives the
remainder with the sign of the dividend, so -7 / 2 is -3 and -7 mod 2 is -1.
A shift moves the bits within the operand’s width, and the bits moved past it
are gone: a uint8 shifted left by six keeps its low eight bits.
An add, a subtract or a multiply whose result leaves its type wraps around in
the type’s width, so a uint8 at 250 plus 10 is 4. The checked build,
--optimize debug,checked or O2,checked (optimization),
plants a guard on each of them instead, and the program stops at the operation
with a trap report. A division or mod by zero stops the program on every tier,
checked or not:
| Written | Where it traps | The report’s reason |
|---|---|---|
an int64 sum past its largest value, big + one | the checked build | reason=signed_arithmetic_overflow (2) |
a uint8 sum past 255, level + 10 | the checked build | reason=unsigned_arithmetic_wrap (3) |
an int64 that a uint8 cannot hold, n as uint8 with n at 300 | the checked build | reason=cast_out_of_range (4) |
a division by a zero variable, a / d | every tier | reason=division_by_zero (1) |
The report’s first line begins Mica runtime failure: and names the reason,
the next names the file, the line and the column of the operation, and the
third shows its source line, as reading a trap report
explains. The traps list every trap and the tier it is
guarded on.
{
Odometer - integer arithmetic in the width of its type. A 'uint8' sum that passes 255 wraps when the build is not
checked, and a constant beside a 'uint8' takes its type. Division drops the fraction toward zero and 'mod' takes
the sign of the dividend. The bit words compute in the operand's own width, so 'not' flips eight bits of a 'uint8'
and 'shl' moves bits out past the eighth. A constant expression is computed exactly while the program compiles.
}
program Odometer;
imp
WriteLn, Ord : std;
const
Rim = 250;
Step = 10;
Seven = 7;
Two = 2;
LowNibble = 0b0000_1111;
Places = 2;
FarPlaces = 6;
FirstBit = 1;
WidePlaces = 40;
SecondsPerDay = 60 * 60 * 24;
NanosecondsPerYear = SecondsPerDay * 365 * 1000000000;
WrapLine = "%lld %lld";
DivisionLine = "%lld %lld %lld %lld";
BitLine = "%lld %lld %lld %lld";
WideLine = "%lld %lld";
ConstantLine = "%lld %lld";
var
level : uint8;
distance : int64;
flags : uint8;
wide : uint64;
day : int64;
begin
{ the sum wraps in eight bits, and the constant one adds in 'uint8' }
level := Rim;
level := level + Step;
WriteLn(WrapLine, level as int64, (level + 1) as int64);
{ division toward zero, the remainder with the sign of the dividend }
distance := Seven;
WriteLn(DivisionLine, -distance / Two, -distance mod Two, distance / -Two, distance mod -Two);
{ the bit words in the eight bits of 'uint8' }
flags := LowNibble;
WriteLn(BitLine, (not flags) as int64, (flags shl Places) as int64, (flags shl FarPlaces) as int64,
(flags shr Places) as int64);
{ a shift in the sixty-four bits of 'uint64', and 'odd' }
wide := (FirstBit as uint64) shl WidePlaces;
WriteLn(WideLine, wide as int64, Ord(odd level));
{ constant expressions, computed exactly while the program compiles }
day := SecondsPerDay;
WriteLn(ConstantLine, day, NanosecondsPerYear);
end.It prints:
4 5
-3 -1 -3 1
240 60 192 3
1099511627776 0
86400 31536000000000000Where the compiler sees the fault in the source, it refuses the program rather than trapping:
| Written | Code | The compiler’s words |
|---|---|---|
a division by the constant zero, a / 0 | 5530 | the divisor of this division is the constant zero, so the operation cannot produce a value: the program is refused here rather than at run time, and at every optimization level alike |
a shift past the width, a shl 64 over an int64 | 5234 | shift count 64 is outside 0..63, the bit positions of 'int64' |
a constant sum a uint8 cannot hold, level := 200 + 100; | 5233 | constant value 300 is outside the domain 0..255 of 'uint8' |
a mod 0 is refused with 5530 in the same words, with modulo in place of
division.
Comparisons and the case test
= and # compare two values of one type that carries equality, and <,
<=, > and >= two values of one ordered type: the integers, the reals, the
ordinal types and strings. Every comparison answers a bool. An integer and a
real do not meet in a comparison, since a float64 holds every int64 exactly
only up to 2^53: the program writes the conversion, a as float64 = ratio,
which as groups before the comparison. Two records or two arrays do not
compare whole, as chapter 4
states.
is tests a value of a sum type for one of its cases,
shape is Circle, which sum types teaches with the
patterns a case test takes.
| Written | Code | The compiler’s words |
|---|---|---|
an integer compared with a real, a = ratio | 5599 | comparison operation 'equal': int64 to float64 is written, never implicit, because float64 holds every integer exactly only up to 2^53 and int64 reaches past it — convert the int64 value with 'as float64', which rounds once where the program asks for it |
a case test of an integer, a is 3 | 5573 | 'is' tests a sum value for one of its cases, and this operand's type is 'int64' |
The boolean operators
and, or and xor take two bool values and not one. and evaluates its
right operand only when the left one is true, and or only when the left one
is false, since otherwise the left one decides the answer; a test that guards
the operation behind it is therefore written in one condition,
count # 0 and total / count > Two. xor always evaluates both operands, and
not negates the relation behind it.
{
Guard - the boolean operators. 'and' evaluates its right operand only when the left one is true, and 'or' only
when the left one is false, so a test that a count is not zero guards the division behind it. 'xor' always
evaluates both operands.
}
program Guard;
imp
WriteLn, Ord : std;
const
Total = 12;
Readings = 4;
Two = 2;
LeftOfAnd = 1;
RightOfAnd = 2;
LeftOfOr = 3;
RightOfOr = 4;
LeftOfXor = 5;
RightOfXor = 6;
NoAverageLine = "no readings, so no average";
AverageLine = "the average %lld is above two";
AskedLine = "asked %lld";
AnswerLine = "%lld";
var
total : int64;
count : int64;
{ An operand that says it was evaluated, then answers 'answer'. }
function Asked(tag : int64, answer : bool) : bool;
begin
WriteLn(AskedLine, tag);
Asked := answer;
end;
{ The average of 'count' readings that add up to 'total', reported only when it is above two. }
procedure Report();
begin
if count # 0 and total / count > Two then
WriteLn(AverageLine, total / count)
else
WriteLn(NoAverageLine);
end;
begin
{ with no readings the division is never evaluated }
total := Total;
count := 0;
Report();
count := Readings;
Report();
{ which operands each operator evaluates }
WriteLn(AnswerLine, Ord(Asked(LeftOfAnd, False) and Asked(RightOfAnd, True)));
WriteLn(AnswerLine, Ord(Asked(LeftOfOr, True) or Asked(RightOfOr, False)));
WriteLn(AnswerLine, Ord(Asked(LeftOfXor, True) xor Asked(RightOfXor, True)));
end.It prints:
no readings, so no average
the average 3 is above two
asked 1
0
asked 3
1
asked 5
asked 6
0Sets and strings
Over two sets of one type, + is the union, * the intersection and - the
difference, and = and # compare two sets whole. in asks whether a member
belongs to a set of its type and answers a bool; not (Sun in work) negates
the question. A set is not ordered, so <= and its kin do not take sets.
Over two strings + joins the text, and = and # compare it whole.
Text teaches the string types.
{
Timetable - the operators of sets and strings. Over two sets of one type '+' is the union, '*' the intersection
and '-' the difference, 'in' asks whether a member belongs, and '=' and '#' compare two sets whole. Over strings
'+' joins the text, and '=' compares it.
}
program Timetable;
imp
WriteLn, Ord, Length : std;
const
Good = "good";
Space = " ";
Morning = "morning";
Expected = "good morning";
MemberLine = "%lld %lld %lld %lld";
CompareLine = "%lld %lld %lld";
TextLine = "%ls %lld %lld";
type
{ The days of the week, Monday first. }
Day = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
{ Any choice of days. }
Days = set of Day;
var
work : Days;
weekend : Days;
every : Days;
middle : Days;
rest : Days;
greeting : string;
begin
{ union, intersection and difference }
work := [Mon..Fri];
weekend := [Sat, Sun];
every := work + weekend;
middle := work * [Tue..Thu, Sat];
rest := work - [Mon];
WriteLn(MemberLine, Ord(Sat in every), Ord(Sat in middle), Ord(Wed in middle), Ord(Mon in rest));
WriteLn(CompareLine, Ord(rest = [Tue..Fri]), Ord(work # every), Ord(not (Sun in work)));
{ a string joined from three and compared whole }
greeting := Good + Space + Morning;
WriteLn(TextLine, greeting, Length(greeting), Ord(greeting = Expected));
end.It prints:
1 0 1 0
1 1 1
good morning 12 1| Written | Code | The compiler’s words |
|---|---|---|
an integer tested against a set of days, a in work | 5115 | incompatible data types in comparison operation 'in': int64 and Days |
a set ordered, work <= [Mon, Tue] | 5116 | data type 'Days' cannot be used in comparison operation 'less equal' (requires ordered) |
a set divided, work / [Mon] | 5112 | incompatible data types in arithmetic operation 'division': Days and set[0..6] of Day |
Conversions with as
as converts the value before it to the type named after it
(cast-expression). It stands
between the adding operators and the comparisons, so it converts a whole sum,
items + 1 as int64, and a converted value compares without parentheses,
count as float64 > limit. A cast that is an operand of an arithmetic operator
stands in parentheses, (level + 1) as int64 inside a larger sum, or
(large as float64) / 2.0. Which conversions each pair of types admits, and
what each does to the value, is
chapter 4 with its refusals; a
narrowing conversion that loses the value traps in the checked build, as the
table of traps above shows.
Constant expressions
An expression of constants is computed while the program compiles, exactly and
alike on every tier: SecondsPerDay * 365 * 1000000000 is the integer
31536000000000000 in the program, with no rounding and no wrap on the way. A
constant takes its type where it is used, as
the type system
states, and a constant value its use cannot hold is refused with 5233, as the
table under integer arithmetic shows. The same table shows the constant
divisor of zero and the constant shift count past the width, which the
compiler refuses rather than leaving to run time.
Calls
A call of a function is an operand
(function-call): its value is
the function’s result, and it stands wherever a value of the result’s type may.
The arguments are separated by commas, and each is evaluated before the
function is entered. Which operands and and or evaluate at all is the rule
of the boolean operators above. How routines are declared and what
their parameters take is
chapter 3.