Technical topic: OpenGEODE - SDL Operators: How to work with data
Contents
Introduction
The SDL language primarily targets the description of a system's behaviour. It is however also possible to manipulate data using a collection of built-in operators. This can be convenient to avoid depending on external, manually-written code to process the parameters of messages during the execution of a state machine transition.
Variables are defined in the ASN.1 language and can express simple types (boolean, numbers and enumerated values) as well as complex data structures (records, arrays, and choices). Each type comes with some dedicated operators which are described in this page.
INTEGER types
MyInt ::= INTEGER (-10 .. 255)
dcl foo MyInt := 42;
The following can be done (in addition to the +, -, / and * operators):
foo := -5 -- assign a value foo := abs (foo) -- return 5 foo := fix (5.7) -- convert float to integer foo := power (10, 2) -- 100 foo := (foo + 1) mod 10 -- modulo write (foo) writeln (foo) -- display the value (procedure call)
Moreover, integer can be assigned with a value expressed with an base 16 and base 2 representation, using the ASN.1 value notation:
foo := '00FF'H; bar := '01100011'B;
If the range of the integer is positive (unsigned integer) then it is also possible to apply bitwise operators to it. In addition to and/or/xor the following operators are also available for unsigned integers:
foo := Shift_Left (foo, 8) bar := Shift_Right (foo, 'FF'H) -- Using the Hex or binary notation is possible too
REAL types
MyReal ::= REAL (0.0 .. 10.0) dcl foo MyReal := 42.0; dcl bar MyInt := 5;
The following can be done:
foo := 0.0 -- assign a value foo := ceil (1.618) -- ceiling foo := floor (1.618) -- ceiling foo := float (bar) -- convert integer to real foo := round (3.14159) -- round foo := sin (3.14) foo := cos (3.14) foo := sqrt (2) foo := trunc (7.77) -- truncation write (foo) writeln (foo)
ENUMERATED types
MyEnum ::= ENUMERATED { foo, bar }
dcl var MyEnum := foo; dcl pos INTEGER (0..1);
Keep in mind that an enumerated is a state and not a number. You can get the value (as a number) of an enumerant like this:
pos := num (var);
If the ENUMERATED type contains explicit values:
MyOtherEnum ::= ENUMERATED {fourty (40), two (2)} dcl baz MyOtherEnum := fourty;
then applying the num operator will return either 40 or 2.
Additionally you can set the value of an enumerated from a position (or from the number set in the ENUMERATED type):
var := val (1, MyEnum) -- sets to bar baz := val (40, MyOtherEnum) -- sets to fourty
You can also print the enumerant name on the screen with the write or writeln operators.
CHOICE types
MyChoice ::= CHOICE { foo BOOLEAN, bar INTEGER (0 .. 255) } dcl a MyChoice := foo : FALSE; -- ASN.1 Value Notation to set the value
You can evaluate the current choice using the present operator in a decision:
DECISION present (a) -- test against foo and bar
writeln (if present (a) = foo then a.foo else a.bar fi)
You can also use an implicit (local) type to store the choice determinant:
dcl current_choice MyChoice_selection := present (a);
At system level you may also have defined a ASN.1 ENUMERATED type with the extact same enumerants as the CHOICE options. This can be useful if you want to send the choice determinant only over an interface, but without its actual value. In that case you can convert the current choice to this enumerated type using the To_Enum operator:
MyDeterminants ::= ENUMERATED { foo, bar } dcl det MyDeterminants := To_Enum (current_choice, MyDeterminants);
In reverse, you can convert the enumerated to the implicit type storing the choice determinant (as returned by the present operator), using the To_Selector operator:
current_choice := To_Selector (det, MyChoice);
Last, there is an internal operator named choice_to_int that can be used if the CHOICE options are (mostly) numerical. It allows to return the value correponding to the current choice item without specifying the field name. You must provide a default value that will be returned in case the current choice's type is not numerical:
dcl someInt MyInt; dcl someChoice MyChoice := { bar : 42 } (...) someInt := choice_to_int (someChoice, 10) -- will return 42 someChoice := { foo : false } someInt := choice_to_int (someChoice, 10) -- will return 10, the default value since the current choice is not "bar"
When combined with other operators, this can be used to make checks on the value of the choice without having to check manually all possible values.
For example you may want to write a generic function that can check a value against a threshold. The value itself could be in a CHOICE:
ValueToMonitor ::= CHOICE { current Amp, voltage INTEGER (0..230) -- and a hundred more } Parameters ::= ENUMERATED { current, voltage, ..... }
but you don't want to write a function that does something like:
if present (val) = current then if val.current < currentThreshold then return true end if else if present (val) = voltage then if val.voltage < voltageThreshold then return true end if else .....
Instead you can do in a single line:
if choice_to_int(val, 0) < thresholds(num(to_enum(val, Parameters))) then return true end if
assuming that threshold is a table indexed in the order of the determinants.
SEQUENCE OF types (arrays)
MySeqOf ::= SEQUENCE (SIZE (0..10)) OF BOOLEAN dcl foo MySeqOf := { true, false, false }; -- ASN.1 Value Notation dcl bar MyInt;
bar := fix (length (foo)) -- 3
for each in foo: call writeln (each); endfor
Bitwise operators (and/or/xor) can be applied to sequences of booleans.
When the array has a variable size (SEQUENCE (SIZE (1..10)) OF ...) the append operator can be applied to add elements:
foo := foo // { true, false }
IMPORTANT
The ASN.1 value notation: { true, false } can only contain literal elements (ground expressions), but not results of a function call or indexed variables - this is NOT allowed:
foo := foo // { foo(1), true }
The proper syntax in that case is to use the "mkstring" operator:
foo := foo // mkstring(foo(1)) // { true }
SEQUENCE types (records)
MySeq ::= SEQUENCE { a BOOLEAN OPTIONAL, b INTEGER (0 .. 255) }
dcl seq MySeq := { a FALSE, b 10 }; -- ASN.1 Value Notation
DECISION exist (seq.a) -- test presence of optional field
STRING types
IA5String
This type is an ASCII string
SomeString ::= IA5String (SIZE(1..255))
It can be assigned a value:
dcl myStr SomeString := 'hello'; -- at declaration TASK myStr := 'world'; -- or in a task TASK myStr := "World"; -- You can use double quotes TASK myStr := 'With \'Escaping\' '; -- And possibly escape inner quotes
It can be used in write/writeln calls.
OCTET STRING
OctStr ::= OCTET STRING (SIZE (0..255))
Octet strings can be assigned a text string, a hex string, or a bit string:
dcl myStr1 OctStr := '68656c6c6f'H; dcl myStr2 OctStr := '01010110'B; dcl myStr3 OctStr := 'hello world';
It is not possible to iterate (with a for loop) directly on OCTET STRINGs. Only SEQUENCE OF types are iterable.
However you can do it using a range iteration:
for each in range(length(myStr1)): call writeln(myStr1(each)); endfor
To add an octet to a variable-size OCTET STRING you can use the append operator as described in the SEQUENCE OF type (see above). However note that to append a numerical variable, the variable has to be cast to an octet using the chr operator:
dcl a SomeInteger := 42; ... myStr1 := myStr1 // mkstring(chr(a))
Symmetrically, if you want to convert an element of an OCTET STRING to an numerical variable, use the fix operator:
for idx in range (length(myStr1)): call crc (fix(tc(idx))); endfor
BIT STRING
BitStr ::= BIT STRING { read(0), write(1), execute(2) } (SIZE (3))
Associating a name to specific bit numbers (in the example: it 0 is "read", bit 1 is "write", and bit 2 is "execute") is optional in ASN.1 but is very useful to assign values.
There are two ways to assign a bit string using the ASN.1 Value notation in OpenGEODE:
dcl a BitStr := '100'B; -- Set the bit 0 (read) to 1 dcl b BitStr := {read, write}; -- Set bits 0 and 1 to 1, using the (optional) bit names
Alternatively, the following type could be defined:
Alternative ::= SEQUENCE { read BOOLEAN, write BOOLEAN, execute BOOLEAN }
However the assignment notation would be more verbose:
dcl c Alternative := {read TRUE, write TRUE};
Keep in mind that when assigning a specific bit, BIT STRINGs are read from left to right (MSB0 notation). Bit 0 is the bit on the left, just like it would be in an array (e.g. SEQUENCE OF BOOLEAN).
If you want to read an individual bit in the string, use an index and use a boolean type (True/False) to store the value.
dcl aBit MyBoolean; -- make sure MyBoolean is defined ... TASK aBit := a(0); -- true if bit 0 is 1
NOTE
The BIT STRING type is supported only by the Ada code generator backend. The C backend does not support this type yet.
TIMER types
timer mytimer;
The SET and RESET operators from SDL are indirectly supported via procedure calls:
set_timer (1000, mytimer); reset_timer (mytimer);
SDL User-defined types
It is possible in the SDL model itself to define custom types. This feature is limited, but useful when the types are to be used only for local purposes and do not need to be exposed to the complete system. OpenGEODE supports a subset of the legacy SDL data types, to define arrays, subtypes of integers (syntypes), and synonyms.
Custom arrays
The syntax is the following, in a text box:
newtype <Type name> array (<Indexing type>, <Element type>) endnewtype;
The indexing type can be an ASN.1-defined INTEGER type with a range, or an ASN.1-defined ENUMERATED type. The element type can be either an ASN.1-defined type, or another newtype-defined type.
If the indexing type is an integer, the tool will convert the new type to a variable-length array using the range of the indexing type:
<Type name> ::= SEQUENCE (SIZE (<Indexing type'Min> .. <Indexing type'Max>)) OF <Element type>
And if the indexing type is an enumerated type, the tool will convert the new type to a fixed sized array using the number of enumerated values as size:
<Type name> ::= SEQUENCE (SIZE (<Number of Enumerated values>)) OF <Element type>
In the SDL model you can index the array either with a raw enumerated value, a variable of the enumerated type, or with a number.
Custom enumerated types
You can create your own enumerated type in plain SDL.
The syntax is the following, in a text box:
newtype <Type name> literals enumerated_value1, enumerated_value2, ... endnewtype;
This is equivalent to the following ASN.1 type:
<Type name> ::= ENUMERATED { enumerated-value1, enumerated-value2, ... }
Syntypes
Syntypes allow to create a range type, that can be combined with a newtype to set the array bounds. It is basically a subtype of an integer type with a different range constraint. The syntax is the following:
syntype <Type name> = <Parent type name> constants comma-separated range_expressions endsyntype;
The parent type can be either an existing user-defined type, or one of the following native types:
- integer (for signed ranges)
- natural (for unsigned ranges)
A range expression can have multiple forms:
min:max a constant /= value (exclude a value) < value <= value > value >= value
Examples:
syntype Foo = MyInteger constants 5:10 endsyntype;
syntype Bar = Foo constants >5, <10 endsyntype;
syntype Age = Natural constants 1:120 endsyntype;
Synonyms
Synonyms in SDL are not really new types. They are rather a convenient way to name a numerical value like a constant.
The syntax is the following:
synonym SP_BASE Word = '0100'H, IRQ_LOCATION Word = 'FFFE'H;
Other features
"FOR" loops
In a TASK box it is possible to specify "for loops" to iterate over elements. This is an extension to the language that is available only in OpenGEODE.
The syntax is:
for <iterator name> in <SEQUENCE OF variable> | RANGE([start], stop, [step]): list of SDL statements endfor -- no semicolon at the end
NOTE: The semantics is the same as the range operator in Python: the last value is excluded from the iteration, meaning that range (4) will yield 4 elements (0, 1, 2, 3) and range (1, 3) will yield 2 elements (1, 2).
The list of SDL statements can contain assignments as well as any SDL symbol that can exist in a transition. In case of simple assignments, no semicolon is needed at the end of a line:
dcl foo MySequenceOfInteger; ... for each in foo: x := x + each -- no semicolon is needed y := y + 1 each := each + 1 -- this statement is useless because "each" is a local variable in this scope (not a modification in place of the array) endfor
However all other statements that use a SDL keyword must be separated by a semicolon:
for each in range (1,20, 2): call writeln (each); y := y + each task x := 5; -- the "task" keyword is optional, but if you use it, you must end the line with a semicolon output message(each); decision each; (>1): output foo(each); else: enddecision; endfor
Ternary operator
The ternary operator in SDL works like the ?: operator in C. It is a shortcut to embed a condition inside another expression.
While in C the syntax is:
x = (y==42) ? 1 : 0
the syntax in SDL is:
x := if y = 42 then 1 else 0 fi