SecreC language reference
Arrays
SecreC is strongly focused on arrays, and the majority of arithmetic, relational and logical operations operate point-wise on them. The main motivation for this behaviour is that private operations are individually slow on some protection domain implementations, and usually require a great deal of network communication overhead. Performing private operations in parallel reduces the time cost involved. The network communication cost is reduced too, as it’s more efficient to send data in bulk rather than sending small packets individually for each operation. SecreC supports multidimensional rectangular arrays. The arrays are more similar to those in Fortran than those in C or Java. The main difference is that multi dimensional arrays in both C and Java are so called Iliffe vectors[1] storing single dimensional vector of pointers to arrays of one dimension less. Like in Fortran arrays in SecreC are always stored as a contiguous block of memory. Every array is associated with sizes of all its dimensions, we call tuple of those a shape of the array. The shape, unlike dimensionality, is a dynamic property and can freely change in the process of program evaluation. The shape is also always a public property, even for arrays containing private data.
Assigning arrays
There are two ways to assign arrays. If the left hand side is a variable then its data and shape are rewritten with that of the right hand side expression. This allows the developer to change the size of an array dynamically. A static check is performed to guarantee that dimensionalities of both sides match. If a scalar is assigned to an array, the assignment is performed point-wise.
Examples of array assignments:
int[[1]] full; // empty right now
int[[1]] arr(10);
full = arr; // no longer empty
arr = 1; // all values set to 1
Array expressions
Arithmetic, logical and relational operations all operate point-wise on arrays. Additionally, where context allows, scalar values are converted into properly sized arrays implicitly. For example scalars at right hand side of array assignment expressions are converted to constant arrays, and all arithmetic, relational and Boolean operators convert scalars to arrays implicitly.
Examples of array expressions:
int[[1]] a(10);
int[[1]] b(10);
// point-wise operations:
a = b + b;
++ a;
// implicit conversions:
b = 2 * a;
a = 5;
Indexing arrays
Indexing of arrays is performed by writing a comma separated list of indices between square brackets after an expression. It’s statically checked that number of indices is equal to the dimensionality of the array being indexed, and that all indices are signed integers of a public security type. Dynamic checks are performed to guarantee that indices are within array bounds. Indexing of variables may also be performed in the left hand side of an assignment. Let us first consider the case of indexing a single dimensional array. Indexing with a public integer returns a value in the array at that position, if it is within the bounds. Run-time error is raised otherwise. Like in C, indices always start at zero.
Indexing arrays:
int[[1]] arr(5) = 1;
int val = arr[0]; // val == 1
arr[1] = 0; // second element of arr is now 0
Any index is allowed to be a slice by denoting it by a colon: separated lower and an upper bound expressions. A slice defines an interval of indices which includes the lower bound of the slice, but excludes the upper bound. The returned value is an array with elements taken from the original array falling between the bounds denoted by that slice. A dynamic check is performed to ensure that all of the elements denoted by the slice are within array bounds. Another way of using slice indexing is in the left hand side of an assignment in which case the region specified by indices is overwritten by the value of the right-hand-side. The assignment is only performed if the shapes of both sides match (an error is raised otherwise). The assignment of a scalar value has the expected point-wise behavior.
Indexing with slices:
int[[1]] arr(5);
arr[1:4] = 1;
arr[2:3] = 2;
// arr == [0 ,1 ,2 ,1 ,0]
There is also some syntactic sugar associated with array slices. If the
lower bound of a slice is missing, it is taken to be the constant zero,
if an upper bound is missing, the size of the array is used. For
example, indexing a one-dimensional array with just a colon returns a
copy of the original array. In case of multiple indices, the indexing is
performed on all of the dimensions in a natural manner. A nice property
of our approach is that it is possible to compute the resulting
dimensionality of the expression by simply counting the number of slices
that the array has been indexed with. Note, that indexing an array
multiple times does not have the same effect as in C. For example, if
mat
is a two-dimensional array, then the expression mat[0][0]
does
not type check as mat
has to be indexed with two indices while only
one is given. The type-correct expression would be mat[0, 0]
. Chaining
indices is still possible. For example, given a vector vec
the
expression vec[1:][2]
is completely legal and returns the fourth
element of the vector.
Array Primitives
In addition to the indexing operators, there are four additional
built-in constructs for manipulating arrays. size
returns the number
of elements in the argument array as a public integer. Size can be
called on expressions of any type. Computing the size of an array takes
— in the worst case — a linear number of multiplications in the number
of dimensions. If invoked on scalars the size expression always
evaluates to 1, and the subexpression will be evaluated.
Size expression:
int[[3]] arr(2, 3, 5);
// size(arr) == 2*3*5
int[[1]] empty ;
// size(empty) == 0
shape
returns the sizes of dimensions of the argument array as a
public integer vector. The type of the argument is not restricted in any
way. If called on scalar value, an empty array is returned. The
subexpression is always evaluated, even if the value of it is not
required.
Shape expression:
int[[3]] arr(2, 3, 5);
int[[1]] s = shape(arr); // == [2, 3, 5]
cat
concatenates the first two argument arrays along the dimension
specified by the third argument. The last argument has to be a public
integer literal. The argument arrays must have equal dimensions and the
same data type. The last argument has to be between zero and the number
of dimensions of arguments. The last argument may be omitted in which
case it is implicitly assumed to be zero.
Concatenation of arrays:
int[[1]] a(5) = 0;
int[[1]] b(5) = 1;
int[[1]] c = cat(a, b); // or cat(a, b, 0)
// size (c) == 10
// c [0:5] == 0
// c [5:10] == 1
Run-time error is raised if shape of concatenated arrays does not match.
reshape
returns an array with the same data, but a different shape
than the original. The first argument is the initial array and the rest
of the arguments specify the new shape. A run-time check is performed to
check that the number of elements in the old and new arrays are equal.
The returned array inherits the values and the security type of the
original array. A common idiom is to combine the use of reshape and size
for flattening multi-dimensional arrays into a vector form.
Array flattening:
int[[2]] mat(5, 5);
int[[1]] arr = reshape(mat, size(mat));
// size(arr) == 25
It is possible to use reshape to create a temporary constant array from a scalar with fixed size. This supplies a convenient tool for changing both the size and value of an already created array, or for constructing temporary constant arrays of given size.
Temporary constant array:
int[[1]] arr(100);
// some computation on arr
arr = reshape(1, 10);
Code structure
A SecreC program starts with a series of global value definitions which are followed by a series of function definitions. Other kinds of statements are not allowed in the global program scope. A SecreC program has to define a function called "main" taking no arguments and returning no value. This function is called when the program is executed by the Sharemind machine. There are two types of comments which are treated as white-space. The single line comments start with two consecutive forwards slashes // and continue until a new line. Multi line comments start with a consecutive forward slash and asterisk and continue up until and including a consecutive asterisk and forward slash. New comments are not started if the starting characters are found within string literals or other comments. Comments count as a white-space, and can be used to separate tokens.
Trivial program:
void main() {
return;
}
Control structures
Most statements in SecreC are separated by a semicolon, and after normal
execution of the statement, control is given to the next statement in
the statement list. Statements can be grouped between curly braces to
form a compound statement. An empty statement is considered a statement.
Expressions ending with a semicolon are also statements and if the
expression evaluates to non-void, the resulting value is discarded. If
the semicolon is not a part of a syntactic construct, such as a variable
declaration or expression statement, it is considered a statement and
has no effect. For example, int i;;
is a composition of a declaration
statement and a skip statement.
If statement
The if-construct is the simplest of the control flow-modifying
statements and executes a statement if the conditional expression
evaluates to true
. To avoid information leakage from the control flow,
the conditional expression is forced to have a public Boolean type.
Multiple statements can be conditionally evaluated with one if statement
by combining them into single block.
If statements:
// general form of if statement:
if (expression)
statement;
// to conditionally execute multiple statements:
if (i > j) {
int t = i;
i = j;
j = t;
}
The if statement may be followed by the else
keyword and another
statement which will only be evaluated if the condition is not met.
While loop
The while statement is the simplest way of looping in SecreC. The body of a while statement is executed repeatedly as long as the condition is met. The conditional expression is evaluated and checked every time before evaluating the statement. For example, if the expression evaluates to false immediately, the statement is not executed at all.
Loop through numbers from 1 to 10:
int i = 1;
while (i <= 10) {
i++;
}
Do-while loop
The do-while construct is very similar to while. The only difference besides syntax is that the condition is checked every time after the execution of the statement instead of before. This means that the statement is evaluated at least once.
Loop through numbers from 1 to 10:
int i = 1;
do {
i += 1;
} while (i <= 10);
For loop
The for statement allows the programmer to specify an initialization expression (or declaration), a conditional expression and a step expression. Initialization is performed before looping is started, the statement is only executed if conditional is true and looping is stopped otherwise. The step expression is executed each time after the statement body.
Loop through numbers from 1 to 10:
for (int i = 1; i <= 10; ++i) {
// this is the body of the statement
}
Every component of the for construct, other than the body, may be
omitted. If the conditional expression is not present, the for statement
is executed as if it was always true. For example infinitely looping
statement can be written as simply as for(;;);
.
Break statement
The break statement ends the execution of current while, do-while or for loops.
Loop through numbers from 1 to 10:
int i = 1;
while (true) {
if (i > 10)
break;
i++;
}
Continue statement
The continue statement skips the execution of the rest of the current while, do-while or for loop iteration and continues with the conditional expression and next iteration if required.
Return statement
The return statement (further discussed in the section about functions) breaks the execution of a function.
Assert statement
The language supports statement for asserting a public Boolean condition. The assert statement is purely for asserting properties that are supposed to hold, but are not immediately obvious. This offers some primitive safety guarantees and eases debugging by failing the program as early as possible in case of incorrect behaviour. If assertion fails the execution of the program is halted.
Assert statement:
int f() { ... }
void main() {
int x = f();
assert(x > 0);
...
}
Expressions
Arithmetic, logical and relational operations
The operators section lists the operators supported on each type in the public and shared3p protection domain kinds.
Operators in SecreC operate on multidimensional arrays point-wise. That means unary operators are applied to all elements of the array and binary operators are applied to all pairs of elements in the same position.
SecreC also implicitly reshapes scalar values to the shape of the other
operand if it is a higher dimensional array. For example if x
is a
vector then x + 1
adds one to each element of x
.
In SecreC, data can flow from the public domain into the private domain.
Public operands can be implicitly classified if the other operand is
private. For example x + 1
is valid even if x
is private.
Programming languages often have short-circuited logical and/or
operators. Computations on private values can not support this because
the running time would leak the value of one of the operands. To
emphasise the fact that &&
and ||
do not work in SecreC as one might
expect, only the bitwise operators &
and |
are supported on private
values. On Booleans they give the same results but are not expected to
be short-circuited. Note that on public values &&
and ||
can not
short-circuit when the operands are vectors but short-circuiting is
still used on public scalars.
Casting
SecreC does not support implicit data type casts, but does support explicit C-style data type conversion. To perform a type cast a parenthesised data type is written before an expression. Non-zero integer typed values cast to true when converting to Boolean values, and zero values cast to false. Conversely, Booleans cast to 1 in case of true and 0 in case of false. Non-public data type conversions are allowed.
Data type conversion example:
int i = 0;
bool b = true;
b = (bool) i;
Type annotation
Due to lack of implicit type casts, and support for function overloading by return type it’s sometimes required to specify a type of an expression explicitly. For example, if a function is overloaded with multiple different return types, an explicit type annotation is required to disambiguate the function call. To annotate an expression with a type, the type is written after double colon following the expression.
Type annotation example:
int f() {
return 1;
}
uint f() {
return 2;
}
void main() {
print(f() :: uint);
}
Ternary operator
Ternary expressions have three subexpressions the first of which must evaluate to a Boolean value. If the value of the first subexpression is true then the value of the second subexpression is the value of the ternary expression. Otherwise, the value of the third subexpression is the value of the ternary expression.
Use of the ternary operator:
int i = 1;
int x = i < 1 ? 0 : 42;
// x == 42
The condition of a ternary expression has to be public. Data and dimensionality types of branches have to be equal. The protection domain of the result is the stricter of the two branches. If one of the branches is a public value then it will be implicitly classified. Ternary expressions operate point-wise if the conditional expression is non-scalar.
Assignment operators
In SecreC, assignments are expressions and thus have a value. For
example, the following code assigns 3 to variable b
.
int a = 1;
int b = a = 3;
Arithmetic assignment expressions are syntactic sugar for changing the
value of a variable using a combination of an arithmetic operator and
assignment. The following example increments a
by 2.
a += 2;
Assignment operators are right-associative and they support implicit operand classification and reshaping like regular arithmetic operators.
Increment/decrement operators
SecreC supports both prefix and postfix increment and decrement
operators. We write i` or `i
to increment the variable i
by one.
The difference between the two expressions is that the value of the
prefix increment expression is the value of the variable after
incrementing while the value of the postfix expression is the value of
the variable before incrementing.
Increment and decrement operators:
int i = 5;
int j;
j = i++; // j == 5, i == 6
j = 42; // j == 42, i == 6
j = --i; // j == 5, i == 5
Declassify operator
The only way to convert a private value into a public value is via the
declassify
operator. In order to forbid expressions such as
declassify(declassify(x))
, the arguments of declassify are not
implicitly classified. declassify
is not safe and all uses of the
declassify
operator should be verified before the SecreC program is
compiled and installed on the servers.
Declassify example:
private int val;
// read the val from database
// do some computation on data
bool result = declassify (val < 0);
declassify
can be used for more efficient implementations of
algorithms by leaking some irrelevant data. For example, the quicksort
implementation in the standard library shuffles the input before sorting
but declassifies results of pairwise comparisons while sorting. Since
the vector is shuffled it is not possible to trace which of the input
elements were compared. This method of sorting is faster than sorting
algorithms that do not use declassification.
Comma operator
The binary comma operator first evaluates its first argument and then evaluates its second argument. The result of the first argument is discarded, and the result of the second argument is returned.
The comma operator is distinct from commas used as separators as in function parameter lists, function argument lists, variable declarations etc. To use the comma operator in such contexts one must first parenthesize the expression or subexpression containing the comma operator. The comma operator is most useful in contexts where additional side-effects are required in an expression, such as in the initialization and increment expressions of a for-loop.
Example of comma operator used in the increment expression of a for-loop:
for (uint count = 0, offset = 10; count < 100; ++count, ++offset)
myArray[offset] = f(i);
Functions
Function (also referred to as a procedure) definitions specify the return type, function name, types and names of the formal parameters, and function body consisting of a list of statements. The same naming rules that applied to variable names apply to function identifiers. All functions have to be defined before they can be called, or in other words, a body of a function can only make calls to itself and to previously defined functions. Functions are global and it is not possible to define nested functions. Arguments to a function call are always evaluated from left to right and are passed by value.
Parameter passing:
void do_nothing(int x, int y) {}
void change(int x) { x = 42; }
void main() {
int x;
do_nothing(x = 1, x = 2);
// x == 2
change (x);
// x == 2
}
The execution of a function can be stopped and values can be returned using a return statement. A return statement breaks the execution of any control-flow structure. If the procedure returns no value the execution of function body is allowed to reach the end of the function. Otherwise, if a function should return a value it has to reach a return statement. Static checks are performed to guarantee that a procedure reaches a return statement and that returned values are appropriately typed.
Every SecreC program has to define a special function called main
with
return type void
taking no parameters. This function is called when
the program execution starts.
Overloading
Functions can be overloaded by the number of parameters, parameter types, or even by return type. For example, it is possible to define functions with the same name for summing values of arrays of different dimensionalities.
int sum(int[[1]] arr) { ... }
int sum(int[[2]] arr) { ... }
void main() {
int[[1]] x;
sum(x); // uses the first one
}
Modules
SecreC has a very simple module system. A module name can be declared
with the module
keyword, and a module can be imported using the
import
keyword. The filename of the module must match the module’s
name. Imported modules are searched from paths specified to the
compiler. Importing a module will make all of the global symbols defined
within the imported module visible. Modules can not be separately
compiled and linked, they simply offer a way to break code into
components.
Module syntax:
module shared3p;
import common;
uint f() {
return 1;
}
Imported symbols will not be exported by a module. If an imported symbol is redefined, a type error is raised. Procedures and templates with the same name but different parameters are considered to be different symbols.
Operator definitions
When defining a protection domain kind we must also define operators if we wish to compute with private values from that kind. Operator definitions are almost like normal function definitions with a special name. For example, we can use a template function with a template domain variable to write a definition for all domains of the same kind. The following is an example of the multiplication operator.
template <domain D : shared3p>
D uint64[[1]] operator * (D uint64[[1]] x, D uint64[[1]] y) {
D uint64[[1]] res (size (x));
__syscall ("shared3p::mul_uint64_vec", __domainid (D), x, y, res);
return res;
}
The compiler can implicitly reshape matrices and scalars into vectors and use a definition with vector arguments. It can also classify one of the arguments if it is public. Thus only a definition with two private vector arguments is required. It is possible to overload a definition. That is, write a definition for some specific combination of argument types. For example, when the second argument is public, we can write the following definition.
template <domain D : shared3p>
D uint64[[1]] operator * (D uint64[[1]] x, uint64[[1]] y) {
__syscall ("shared3p::mulc_uint64_vec", __domainid (D), x, __cref y, x);
return x;
}
This definition will be preferred by the compiler when the supplied second argument is public.
Note that it is not possible to add new operators. Only built-in
operators can appear after the operator
keyword.
We must also define type conversions (cast operators) similarly. An example follows.
template <domain D : shared3p>
D bool[[1]] cast (D uint64[[1]] x) {
D bool[[1]] res (size (x));
__syscall ("shared3p::conv_uint64_to_bool_vec", __domainid (D), x, res);
return res;
}
Operators
The following table lists SecreC operators including associativity and precedence (from highest to lowest).
Level | Operator(s) | Description | Associativity |
---|---|---|---|
14 |
|
Assignment operators |
Right |
13 |
|
Ternary operator |
Left |
12 |
|
Logical disjunction |
Left |
11 |
|
Logical conjunction |
Left |
10 |
|
Bitwise or |
Left |
9 |
|
Bitwise xor |
Left |
8 |
|
Bitwise and |
Left |
7 |
|
Relational operators |
- |
6 |
|
- |
|
5 |
|
Bitshift operators |
Left |
4 |
|
Arithmetic operators |
Left |
3 |
|
Left |
|
2 |
|
Increment operator |
- |
1 |
|
Decrement operator |
- |
0 |
|
Bitwise/logical/ arithmetic negation |
Right |
|
Array access |
||
|
Function call |
public protection domain
Supported types: bool
, uint8
, uint16
, uint32
, uint
, int8
,
int16
, int32
, int
, float32
, float64
.
Operators
Type | Operators |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shared3p protection domains
Supported types: bool
, uint8
, uint16
, uint32
, uint
, int8
,
int16
, int32
, int
, float32
, float64
, xor_uint8
,
xor_uint16
, xor_uint32
, xor_uint64
.
Operators
Type | Operators |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Performance of shared3p protocols
ℹ️ Content in this sub-section is adapted from the dissertation "Programming Languages for Secure Multi-party Computation Application Development" by Jaak Randmets (2017).
This section contains a table with performance measurements of shared3p protocols on different data types and input sizes. The results are highly dependent on the hardware and network setup so do not expect to get the same results. This table can be used to understand the performance differences of protocols when optimising programs. The table is missing integer addition because it requires no network communication. The unit is thousands of operations per second.
Operation | Type | 1 | 10 | 100 | 1000 | 10000 | 100000 | 1000000 | 10000000 |
---|---|---|---|---|---|---|---|---|---|
BitExtraction |
uint8 |
1.75 |
18 |
168 |
1410 |
5000 |
6670 |
8290 |
8320 |
BitExtraction |
uint16 |
1.28 |
12.5 |
119 |
931 |
3370 |
3260 |
3930 |
4200 |
BitExtraction |
uint32 |
1.62 |
15.9 |
138 |
852 |
1780 |
1390 |
1560 |
1660 |
BitExtraction |
uint64 |
1.25 |
11.4 |
93.6 |
498 |
683 |
601 |
663 |
642 |
Ceiling |
float32 |
0.532 |
4.65 |
25.9 |
39.6 |
35.7 |
39.6 |
40.6 |
|
Ceiling |
float64 |
0.464 |
3.67 |
9.87 |
10.2 |
10.3 |
12.4 |
13.1 |
|
Comparison |
uint8 |
1.3 |
12.8 |
111 |
681 |
1740 |
1860 |
2080 |
2100 |
Comparison |
uint16 |
1.45 |
14.2 |
116 |
582 |
1200 |
1130 |
1270 |
1320 |
Comparison |
uint32 |
1.07 |
10.3 |
80 |
407 |
710 |
570 |
619 |
622 |
Comparison |
uint64 |
1.1 |
10.1 |
71.7 |
282 |
311 |
265 |
308 |
293 |
Comparison |
xor_uint8 |
2.37 |
23.7 |
214 |
1510 |
4760 |
6140 |
5950 |
5830 |
Comparison |
xor_uint16 |
1.92 |
19.3 |
164 |
1080 |
2980 |
3220 |
3480 |
3230 |
Comparison |
xor_uint32 |
1.69 |
16.5 |
134 |
799 |
1640 |
1470 |
1400 |
1420 |
Comparison |
xor_uint64 |
1.6 |
14.7 |
111 |
577 |
895 |
620 |
639 |
626 |
Division |
uint8 |
0.665 |
6.18 |
46.6 |
179 |
217 |
172 |
174 |
175 |
Division |
uint16 |
0.636 |
5.75 |
40.6 |
122 |
113 |
108 |
114 |
110 |
Division |
uint32 |
0.478 |
3.75 |
13.1 |
18.6 |
18.7 |
24.2 |
24 |
24.5 |
Division |
uint64 |
0.393 |
2.38 |
5.95 |
6.56 |
7.57 |
9.23 |
9.54 |
9.75 |
Equality |
uint8 |
2.69 |
26.5 |
259 |
2080 |
8210 |
10300 |
11600 |
11600 |
Equality |
uint16 |
2.26 |
22.3 |
218 |
1730 |
6910 |
8470 |
9500 |
9380 |
Equality |
uint32 |
1.99 |
19.5 |
186 |
1460 |
5200 |
5170 |
5350 |
5450 |
Equality |
uint64 |
1.83 |
18 |
164 |
1220 |
3830 |
3040 |
3250 |
3310 |
Equality |
xor_uint8 |
3.42 |
34.9 |
311 |
1800 |
3370 |
3960 |
3480 |
3410 |
Equality |
xor_uint16 |
2.79 |
27.3 |
237 |
1020 |
1600 |
1800 |
1680 |
1880 |
Equality |
xor_uint32 |
2.59 |
24.9 |
197 |
675 |
909 |
945 |
825 |
936 |
Equality |
xor_uint64 |
2.16 |
20.6 |
139 |
342 |
440 |
401 |
422 |
467 |
FixInv |
fix32 |
0.209 |
1.46 |
3.68 |
4.33 |
5.82 |
7.11 |
7.19 |
|
FixInv |
fix64 |
0.162 |
0.747 |
1.03 |
1.28 |
1.76 |
2.09 |
2.08 |
|
FixMultiplication |
fix32 |
0.681 |
6.45 |
51.4 |
274 |
383 |
350 |
377 |
|
FixMultiplication |
fix64 |
0.763 |
7.27 |
50.3 |
183 |
199 |
180 |
195 |
|
FixSqrt |
fix32 |
0.256 |
2.19 |
10 |
16.6 |
17.7 |
20.3 |
20.7 |
|
FixSqrt |
fix64 |
0.193 |
1.38 |
3.8 |
4.83 |
5.17 |
6.5 |
6.71 |
|
FloatAddition |
float32 |
0.326 |
3.06 |
20.8 |
53.6 |
59.7 |
50.6 |
50.9 |
|
FloatAddition |
float64 |
0.294 |
2.58 |
13.3 |
23.4 |
23.9 |
24.6 |
24.6 |
|
FloatCompare |
float32 |
1.21 |
11.5 |
83.6 |
218 |
226 |
198 |
217 |
|
FloatCompare |
float64 |
1.13 |
10.2 |
67.1 |
127 |
114 |
108 |
120 |
|
FloatErf |
float32 |
0.247 |
2.17 |
12.7 |
23.6 |
24 |
23.1 |
23.5 |
|
FloatErf |
float64 |
0.18 |
1.37 |
4.17 |
5.61 |
5.51 |
6.48 |
6.87 |
|
FloatExp |
float32 |
0.203 |
1.88 |
12.3 |
29.4 |
33.5 |
29.5 |
29.5 |
|
FloatExp |
float64 |
0.161 |
1.34 |
5.39 |
8.92 |
8.61 |
9.35 |
9.72 |
|
FloatInv |
float32 |
0.297 |
2.69 |
17.9 |
47 |
51.2 |
46.3 |
47.9 |
|
FloatInv |
float64 |
0.232 |
1.96 |
9.18 |
16.1 |
17 |
17.4 |
18.1 |
|
FloatLn |
float32 |
0.14 |
1.23 |
6.86 |
11.9 |
12.1 |
11.1 |
11.4 |
|
FloatLn |
float64 |
0.124 |
0.979 |
3.28 |
4.17 |
4.05 |
4.42 |
4.61 |
|
FloatMultiplication |
float32 |
0.549 |
5.19 |
37.9 |
143 |
220 |
171 |
177 |
|
FloatMultiplication |
float64 |
0.548 |
4.94 |
33.1 |
107 |
121 |
106 |
108 |
|
FloatSin |
float32 |
0.147 |
1.29 |
6.27 |
8.93 |
8.62 |
8.76 |
9.18 |
|
FloatSin |
float64 |
0.126 |
0.951 |
2.46 |
2.79 |
2.71 |
3.42 |
3.44 |
|
FloatSqrt |
float32 |
0.284 |
2.59 |
16.9 |
43.9 |
48.8 |
42.7 |
43.7 |
|
FloatSqrt |
float64 |
0.216 |
1.76 |
6.4 |
9.98 |
9.17 |
10.9 |
11.1 |
|
Floor |
float32 |
0.53 |
4.71 |
25.8 |
40.7 |
35.2 |
38 |
39.9 |
|
Floor |
float64 |
0.465 |
3.62 |
10.3 |
10.4 |
10.6 |
12.9 |
13.2 |
|
Multiplication |
uint8 |
6.61 |
64.1 |
644 |
5550 |
26900 |
44400 |
40700 |
32900 |
Multiplication |
uint16 |
6.13 |
61.9 |
591 |
5160 |
24700 |
38200 |
34100 |
35200 |
Multiplication |
uint32 |
5.1 |
52.4 |
508 |
4190 |
17400 |
20900 |
21400 |
23000 |
Multiplication |
uint64 |
5.03 |
50.5 |
458 |
3900 |
11700 |
13000 |
11400 |
12600 |
PrivateShiftLeft |
uint8 |
2.47 |
24.5 |
236 |
1690 |
4750 |
4460 |
4920 |
4840 |
PrivateShiftLeft |
uint16 |
2.42 |
24.1 |
226 |
1590 |
2350 |
1810 |
2000 |
2000 |
PrivateShiftLeft |
uint32 |
2.37 |
23.9 |
213 |
802 |
619 |
544 |
607 |
597 |
PrivateShiftLeft |
uint64 |
2.26 |
22 |
163 |
185 |
158 |
189 |
192 |
190 |
PrivateShiftRight |
uint8 |
0.399 |
4.98 |
41.7 |
377 |
1500 |
1860 |
2000 |
2040 |
PrivateShiftRight |
uint16 |
0.759 |
7.26 |
68.6 |
472 |
1030 |
911 |
1120 |
1140 |
PrivateShiftRight |
uint32 |
0.555 |
5.43 |
49.2 |
311 |
413 |
390 |
448 |
462 |
PrivateShiftRight |
uint64 |
0.319 |
3.49 |
22.6 |
102 |
108 |
130 |
141 |
141 |
PublicDivision |
uint8 |
0.746 |
7.47 |
66 |
469 |
1490 |
1750 |
1960 |
2000 |
PublicDivision |
uint16 |
0.551 |
5.46 |
49.5 |
322 |
1020 |
953 |
1030 |
1040 |
PublicDivision |
uint32 |
0.56 |
5.5 |
49.8 |
271 |
563 |
480 |
526 |
516 |
PublicDivision |
uint64 |
0.506 |
4.47 |
34.1 |
150 |
211 |
179 |
183 |
177 |
PublicShiftRight |
uint8 |
0.786 |
8.53 |
79.1 |
726 |
3030 |
4220 |
5230 |
5810 |
PublicShiftRight |
uint16 |
0.909 |
8.82 |
86.6 |
714 |
2540 |
2520 |
3110 |
3160 |
PublicShiftRight |
uint32 |
1.22 |
12 |
105 |
706 |
1450 |
1190 |
1380 |
1410 |
PublicShiftRight |
uint64 |
1.08 |
10.2 |
83.2 |
465 |
598 |
532 |
629 |
607 |
Structures
SecreC has basic support for structures. The types of structure fields is not restricted: it’s possible to store other structures, arrays and private data within a structure. Structures themselves, however, are limited in multiple ways. Namely, they are restricted to be public data and may not not be stored as elements of arrays.
For example, the following code defines a structure with two integer fields:
struct elem2d {
int x;
int y;
}
public elem2d v;
v.x = 1;
v.y = 2;
The language also supports polymorphic structures. A structure may have various type-level parameters that all need to be specified whenever a structure with that name is used. The previous example can be declared type-polymorphically in which case, when defining data of that type, the type parameter has to be specified as well.
template <type T>
struct elem2d {
T x;
T y;
}
public elem2d<int> v;
v.x = 1;
v.y = 2;
Structures may also be polymorphic over protection domains.
template <domain D, type T>
struct elem2d {
D T x;
D T y;
}
public elem2d<pd_shared3p, int> v;
v.x = 1;
v.y = 2;
Templates
In order to support domain type polymorphic functions the language has
support for C++ style function templates. Function templates give the
language static domain, type and array dimension polymorphism. Templates
are declared using the template
keyword, and the template domain, type
and dimension parameters are listed between < and > brackets.
Simple template declaration:
template <domain D, type T, dim N>
D T minimum (D T[[N]] arr) { ... }
Templates may restrict a domain variable to a protection domain kind:
kind shared3p;
template <domain D : shared3p, type T>
T declassify (D T x) { ... }
Function templates are not typechecked. For example they can refer to undefined variables in the function body and this is not a type error. Template instances are typechecked after the template quantifier variables are replaced with concrete types. If multiple functions match a function call a priority system is used to select a single match. First the number of template variables are compared, next the number of template variables that are not restricted with domain kind are compared, and finally the number of quantified function domain type parameters are compared. If there’s no least match according to this partial ordering a type error is raised. The intuitive idea behind the resolution system is that the most specialized template or procedure is selected. For example, reclassification can be defined between two potentially different domains, but also within a single domain. If so, then the version that uses a single template variable is selected whenever possible.
Reclassify:
template <domain D : shared3p , domain L : shared3p>
D int reclassify (L int x) { ... }
template <domain D>
D int reclassify (D int x) { return x; }
Types
SecreC is strongly and statically typed. There are two features that
make SecreC type system unique. The type system is strongly focused on
arrays, and all primitive data has a privacy (security) type. Types in
SecreC start with an optional security type, followed by the primitive
data type and finally an optional array dimensionality type between
double square brackets. For example, we can write: int
for integers,
d bool
for Booleans in security domain d
, or public int[[5]]
for
public 5-dimensional integer array. The default security type is public,
and the default dimensionality type is scalar.
Protection domains
SecreC has a built-in public
domain and user defined private
protection domains. Public types can be optionally annotated with the
keyword public
, and non-public types are annotated with the name of a
protection domain. Domains are declared in global scope using the
keyword domain
. Every protection domain belongs to some protection
domain kind. The distinction between domains and kinds is necessary
because it’s possible to have data in different protection domains of
the same kind. Protection domain kinds are defined in global scope using
the kind
keyword. A kind definition gives a name to the kind and lists
the data types supported by the kind. A data type definition has a name
and an optional corresponding public type for expressions where public
values are implicitly classified such as adding a private and a public
value. For types with a name matching a public data type, the
corresponding public type must be the matching type. The public type
argument can be omitted for built in data types. In that case the public
type with the same name will be the corresponding public type. Most
users should not need to define a kind as this is provided by the
implementer of the protocol set. The following is an example definition
of a kind with the data types bool
, uint64
and xor_uint64
(non-standard type):
kind shared3p {
type bool; // public type will be bool
type uint64 { public = uint64 };
type xor_uint64 { public = uint64 };
}
To declare two domains that belong to the same kind we could write the following in the global scope:
domain sharemind_test_pd shared3p;
domain my_pd shared3p;
Protection domains form a lattice where the public domain is ordered before every private domain. The lattice is used to formulate typing rules which allow public values to be used in private contexts where they are implicitly classified. It’s also used to prevent private values from being used in public contexts.
Primitive types
SecreC has the following primitive data types:
-
int
,int64
,int32
,int16
andint8
for signed integers, -
uint
,uint64
,uint32
,uint16
anduint8
for unsigned integers, -
bool
for Booleans, eithertrue
orfalse
, -
float32
,float
andfloat64
for floating point numbers, -
string
for strings.
Integer types int
, and uint
are synonyms for 64-bit signed, and
unsigned integer types. Floating point type float
is synonymous with
32-bit floating point type.
These types are supported by the public protection domain. Every private
protection domain kind defines its own set of primitive types. It can
overlap with the types of the public domain but there can be
non-standard types such as the xor_uint64
type in the shared3p
protection domain kind. Kind definitions are described in the
Protection domains section.
Array types
The dimensionality (number of dimensions of an array) of a value is declared in the type by writing an integer literal between double square brackets. For example, we write [[1]] to declare one-dimensional vectors, and [[2]] for declaring matrices. It is possible to denote scalars by [[0]], but it’s more concise to omit the annotation. The number of dimensions is not limited. Dimensionality is a static property and never changes throughout the life of a variable. This is guaranteed by the type system. We often call a one-dimensional array a vector, and a two-dimensional array a matrix.
Variables
Variables in SecreC consist of lowercase and uppercase Latin characters, underscores and decimal digits. The first character of a variable must not be a decimal digit. Reserved keywords are not allowed to be used as variable names.
Declaring and defining
Variables are declared by a type annotation followed by one or more
variable names. Optionally, it is possible to assign a value right after
the variable declaration by writing an expression after the assignment
sign. Uninitialized variables are assigned default values. For integers
this value is 0, for Booleans it is false
.
Some variable declarations:
kind shared3p {
type bool;
type uint8;
}
domain private shared3p;
void main() {
int x; // assigned default value
int y = 5;
private uint8 z;
private bool secret = true;
uint i, j = 2, k;
return;
}
The shape of an array can be specified in parenthesis after the variable name. For example:
int[[1]] vector(100); // vector of 100 elements
bool[[2]] mat(3, 4) = true; // constant true 3x4 matrix
int n;
int[[3]] cube(2 * n, 3 * n, 4 * n);
It is possible to define an empty array by not specifying the shape. If the array has no shape specification but is initialized with an expresssion then the shape is inherited from the value of the expression.
int[[1]] empty; // empty array
int[[1]] x(10);
int[[1]] notEmpty = x;
Scope
The scope of a variable always ends with the enclosing statement block. Variables with the same name can not be declared within the same scope, but can be shadowed by declaring a new variable with same name in a deeper nested scope. Global variables never fall out of scope, and can not be shadowed. Protection domains and kinds can not be shadowed. Variables with the same names can be declared in non-overlapping scopes.
Variable shadowing:
int x = 1;
{ // nested scope
int x;
int y = 1;
x = 5;
}
// x == 1
// y is not reachable