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

bool

>, <, >=, , ==, !=, &&, ||, &, |, ^, !

uint8

+, -, *, /, %, >, <, >=, , ==, !=, &, |, ^, ~, <<, >>

uint16

+, -, *, /, %, >, <, >=, , ==, !=, &, |, ^, ~, <<, >>

uint32

+, -, *, /, %, >, <, >=, , ==, !=, &, |, ^, ~, <<, >>

uint64

+, -, *, /, %, >, <, >=, , ==, !=, &, |, ^, ~, <<, >>

int8

+, -, *, /, >, <, >=, , ==, !=

int16

+, -, *, /, >, <, >=, , ==, !=

int32

+, -, *, /, >, <, >=, , ==, !=

int64

+, -, *, /, >, <, >=, , ==, !=

float32

+, -, *, /, >, <, >=, , ==, !=

float64

+, -, *, /, >, <, >=, , ==, !=

Casts

In the public domain, values can be cast from every type to any other type.

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

bool

==, !=, &, |, ^, !

uint8

+, -, *, /, %, >, <, >=, , ==, !=, <<, >>

uint16

+, -, *, /, %, >, <, >=, , ==, !=, <<, >>

uint32

+, -, *, /, %, >, <, >=, , ==, !=, <<, >>

uint64

+, -, *, /, %, >, <, >=, , ==, !=, <<, >>

int8

+, -, *, /, >, <, >=, , ==, !=

int16

+, -, *, /, >, <, >=, , ==, !=

int32

+, -, *, /, >, <, >=, , ==, !=

int64

+, -, *, /, >, <, >=, , ==, !=

float32

+, -, *, /, >, <, >=, , ==, !=

float64

+, -, *, /, >, <, >=, , ==, !=

xor_uint8

>, <, >=, , ==, !=, &, |, ^, ~, <<, >>

xor_uint16

>, <, >=, , ==, !=, &, |, ^, ~, <<, >>

xor_uint32

>, <, >=, , ==, !=, &, |, ^, ~, <<, >>

xor_uint64

>, <, >=, , ==, !=, &, |, ^, ~, <<, >>

Casts

Source type(s) Target types

bool

Any type

uintN

bool, unsigned integers, signed integers, floating-point types, xor_uintN

intN

bool, unsigned integers, signed integers, floating-point types, xor_uintN

floatN

bool, unsigned integers, signed integers, floating-point types

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 and int8 for signed integers,

  • uint, uint64, uint32, uint16 and uint8 for unsigned integers,

  • bool for Booleans, either true or false,

  • float32, float and float64 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

References


1. J. Iliffe, "The use of the genie system in numerical calculation," Annual Review in Automatic Programming, vol. 2, pp. 1–28, 1961.