The Sen Type Language (STL)¶
STL is Sen's small declarative language for describing the shape of components: their configurable data, their events, and the operations they expose.
Looking for the exact syntax rules? See the formal grammar reference for tokens, EBNF production rules, and the full list of registered quantity units.
This is an example:
package sensors;
// Periodically reports temperature from a configured hardware source.
class TemperatureSensor
{
var modelName : string [static]; // configured once per instance
var sampleRate : f32 [writable]; // sampling rate in Hz
// Read the current temperature, in degrees Celsius.
fn read() -> f32 [const];
// Emitted whenever a new sample is available.
// @param value the freshly sampled temperature
event sampleReady(value: f32) [confirmed];
}
File structure¶
The structure of STL files is as follows:
- Imports
- Package name
- Types
Imports are a way to include other STL files, so you can use those definitions. For example:
You can import as many STL files as you need.
STL files specify a package, which is a namespace where the types will be held. There can only be one package declaration per STL file. Multiple STL files might declare the same package. For example:
To use a type defined in another package, import the STL file that declares it
and reference the type by its qualified name (<package>.<typename>):
import "stl/sen/kernel/basic_types.stl"
package my_app;
struct ComponentHealth
{
name : string,
build : sen.kernel.BuildInfo // qualified name
}
Basic types¶
Sen defines the following basic types:
| Name | Description | C++ |
|---|---|---|
u8 |
8-bit unsigned integral. | uint8_t |
u16 |
16-bit unsigned integral. | uint16_t |
i16 |
16-bit integral. | int16_t |
u32 |
32-bit unsigned integral. | uint32_t |
i32 |
32-bit integral. | int32_t |
u64 |
64-bit unsigned integral. | uint64_t |
i64 |
64-bit integral. | int64_t |
f32 |
32-bit floating point. | float |
f64 |
64-bit floating point. | double |
bool |
Boolean. | bool |
string |
Character string. | std::string |
TimeStamp |
A point in time. | TimeStamp |
Duration |
A time duration. | Duration |
Literals¶
Literals appear in attribute values ([min: -90.0], [tag: my_tag]) and
inside enum storage declarations. STL's literal syntax is deliberately
minimal:
- Strings are double-quoted (
"hello") or single-quoted ('hello'). Escape sequences (\n,\t, ...) are not interpreted; the characters between the quotes are taken literally. - Integers are decimal only, optionally prefixed with
-. Hexadecimal (0x...), octal, binary, exponent forms, and type suffixes are not supported. - Floats require a decimal point. Write
1.0, not1; the decimal point is what distinguishes a float from an integer. - Booleans are
trueandfalse.
Value types vs classes¶
Most of STL is about value types: types whose instances can be copied and transferred wholesale between components, across the network, or into storage. The following are value types:
- Basic types (
u32,bool,string,TimeStamp,Duration, ...) - Containers (
sequence,array,optional,quantity) - Custom types declared with
enum,struct,variant,alias
Classes are not value types. A class represents a live element with
identity and behavior; it cannot appear as a property type, a struct field,
a variant alternative, a method parameter, or a return value. When you need
to reference another object from a value-typed context, store an identifier
(a name, an ID, or some other differentiator) and resolve it at query time
with a SELECT statement.
Sequences and arrays¶
Sequences are lists of elements. They can be bounded or unbounded and store any value type.
| Name | Description | C++ |
|---|---|---|
sequence<T> |
Unbounded sequence of T. | std::vector<T> |
sequence<T, size> |
Bounded sequence of T. | sen::StaticVector<T, size> |
array<T, size> |
Fixed size sequence of T. | std::array<T, size> |
For example, you can define an unbounded sequence as follows:
Unbounded sequences have variable size and unlimited capacity. This means that they can grow and shrink, and their contents are limited by the amount of available memory. They use the heap.
Bounded sequences have fixed capacity and variable size. This means that you can add and remove elements, but only until you reach the maximum capacity. They use the stack.
Note that bounded sequences have similarities with arrays (fixed capacity, stack usage), but behave like vectors (or lists), in the sense that they have a size which increases or decreases when you add or remove elements.
Arrays, by contrast, have fixed length and do not grow or shrink:
Optional values¶
You can define types that might optionally hold a value (of any given type). For example,
Quantities¶
You can use strongly-defined quantity types, with units and optional ranges. For example,
quantity<f64, deg> Lat [min: -90.0, max: 90.0];
quantity<f64, deg> Lon [min: -180.0, max: 180.0];
quantity<f32, m_per_s> Speed;
quantity<f32, rad> Angle;
quantity<f32, m> Meters;
The second type argument is the unit abbreviation (m, rad, deg, kph,
degC, ...), not the long form. SI base units are also registered with every
metric prefix, so km, ms, us, cm, MPa all resolve out of the box.
See the full list of registered units for what is currently available.
Quantities accept an attribute list containing min: and/or max: bounds,
as shown above.
Aliases¶
Aliases give an existing type a new name. They are useful for making intent clear,
or for hiding implementation details behind a domain-specific name. The alias
is structural - DeviceId below is interchangeable with u64 wherever it is
used.
Note that there is no = between the alias name and the aliased type; the
two identifiers sit side-by-side.
For example,
alias DeviceId u64;
alias NameList sequence<string>;
alias CoordinatePair array<f64, 2>;
alias HostBuildInfo sen.kernel.BuildInfo; // aliasing a struct from another package
Aliases shine once you've declared your own structs, variants, and enums: any of them can be given a second name for clarity at the call site.
Enumerations¶
In STL, enumeration types are defined like this:
For example,
// Category of a problem during component execution
enum ErrorCategory: u8
{
runtimeError, // same semantics of std::exception
logicError, // faulty logic such as violating logical preconditions or invariants
expectationsNotMet, // expectations on inputs or internal state were not met
ioError, // problem while performing an I/O operation
other // any other reason
}
The storage type must be an integral (u8,u16,i16,u32,i32, u64 or i64).
Structures¶
You can group values with structs. They are defined like this:
Fields can be of any type and names must be unique.
For example:
// Build-related information
struct BuildInfo
{
maintainer: string, // principal maintainer of the software
version : string, // version string (format-agnostic)
compiler : string, // vendor-specific compiler string
debugMode : bool, // compiled in debug mode or not
buildTime : string, // when did this build took place
wordSize : WordSize, // architecture
gitRef : string, // git ref spec
gitHash : string, // git hash
gitStatus : GitStatus // git status
}
In C++, structs are (unsurprisingly) rendered as struct.
If your struct does not have any field, you can omit the { }. For example,
Furthermore, structs may have a parent struct to avoid code duplication. For example,
struct ParentStruct
{
familyName : string,
hairColor : string,
badHabits : sequence<BadHabit>
}
struct ChildStruct : ParentStruct
{
name : string,
age : u8,
}
Note: structs that specify a parent always declare a is-a relationship
to their parent. That is, as structs do not have any invariants all data members from
the parent will be available to every user of the derived class. Furthermore, a struct
with a parent is a class that requires run-time polymorphism and should, therefore, also
be treated as such in code. We currently strongly discourage the polymorphic usage of
structs as parent structs do have a virtual constructor.
Variants¶
The variant type represents a type-safe union. A variant instance always holds a value of exactly one of its alternative types. As with unions, the object representation of the held type is allocated directly within the variant's own object representation, with no additional (dynamic) memory involved. A variant is not permitted to list the same type more than once.
They are defined as follows:
Types must be unique within the variant.
For example,
variant CustomTypeData
{
EnumTypeSpec,
QuantityTypeSpec,
SequenceTypeSpec,
StructTypeSpec,
VariantTypeSpec,
AliasTypeSpec,
ClassTypeSpec
}
Note that variants can be used as "enumerations with state". For example:
struct HideCursor;
struct ShowCursor;
struct SaveCursorPosition;
struct RestoreCursorPosition;
struct MoveCursorLeft { cells: u32 }
struct MoveCursorRight { cells: u32 }
struct MoveCursorUp { cells: u32 }
struct MoveCursorDown { cells: u32 }
struct Print { text : string }
struct CPrint
{
flags : u32,
color : u32,
text : string
}
variant TerminalCommand
{
HideCursor,
ShowCursor,
SaveCursorPosition,
RestoreCursorPosition,
MoveCursorLeft,
MoveCursorRight,
MoveCursorUp,
MoveCursorDown,
Print,
CPrint
}
In C++, variants are rendered as std::variant<...>.
Classes¶
You can define classes as follows:
For example:
A class may extends at most one parent class.
If a class is marked as abstract, the Sen kernel will refuse to instantiate
it without a dedicated C++ implementation. Non-abstract classes can be
instantiated directly from configuration.
There are 3 kinds of members that a class can have: properties, methods and events.
Methods are defined as follows:
For example,
class Example
{
// a method with no arguments (that doesn't return)
fn methodWithoutArgs() [const, bestEffort];
// a method that returns a string
fn methodThatReturns() -> string [const];
// adds two integrals
fn add(leftHandSide: i32, rightHandSide: i32) -> i32;
}
Events are defined as follows:
For example,
class Example
{
// an event without arguments
event somethingHappened();
// an event with arguments
event somethingElseHappened(what: string, count: u32);
}
Properties are defined as follows:
For example,
class Person
{
var firstName : string [static];
var surName : string [static];
var kind : string [static_no_config];
var stressLevel : f32 [writable];
var temperature : f32 [writable];
var brainActivity : f32;
}
The attribute vocabulary for properties, methods, and events is listed in the Attributes section. Remember that property types, method parameters, and return types must be value types.
Attributes¶
Attributes decorate a declaration with metadata. They appear in square brackets
at the end of the declaration, before the ;, and are separated by commas.
Most attributes are flags: their name on its own turns the flag on. A few
take a value, written as name: value (e.g. min: 0.0, tag: my_tag).
The table below lists every attribute and which declarations it applies to.
| Attribute | Property | Method | Event | Quantity | Meaning |
|---|---|---|---|---|---|
static |
✓ | The property never changes during the lifetime of the object. | |||
static_no_config |
✓ | Static and cannot be set from YAML configuration; only from the implementation. | |||
writable |
✓ | The property has a public setter (it can be set externally). | |||
confirmed |
✓ | ✓ | ✓ | Transport is reliable. Default for methods; opt-in for properties and events. | |
bestEffort |
✓ | ✓ | ✓ | Transport uses best-effort mechanisms. Default for events; opt-in for methods/properties. | |
const |
✓ | The method does not change the state of the object. | |||
local |
✓ | The method can only be called within its component context. | |||
tag: <name> |
✓ | User-defined tag, inspectable at runtime. May appear multiple times. | |||
min: <literal> |
✓ | Lower bound for the quantity's value. | |||
max: <literal> |
✓ | Upper bound for the quantity's value. |
Example combining several:
var stressLevel : f32 [writable, tag: bio, tag: humanitarian];
fn probe() -> string [const, local];
event beaconFired() [confirmed];
Properties do not support a default-value syntax (var x : u32 = 5 [static]; is
not valid). Initial values come from the component's YAML configuration, or from
the implementation at construction time.
Tagging properties¶
The tag: <name> attribute is the mechanism for user-defined labelling of
properties. Tags are inspectable at runtime through Sen's meta-reflection API
and let you group properties across unrelated classes for filtering,
selection, or UI purposes, without those classes having to share a common
ancestor. A property may carry multiple tags:
class Patient
{
var heartRate : f32 [writable, tag: vital, tag: continuous];
var bloodPressure : f32 [writable, tag: vital];
var lastVisit : TimeStamp [tag: audit];
}
class Incubator
{
var internalTemp : f32 [writable, tag: vital, tag: continuous];
}
A query can then ask for "every vital-tagged property across the system"
and get back heartRate, bloodPressure, and internalTemp in a single
result, regardless of which class they belong to.
Customizing the generated code¶
If you are generating C++ code, there are some knobs you can use to customize the output:
- Checked properties: If you have a
writableproperty, you can tell Sen to first ask for your approval when someone attempts to do a "set" to it. - Deferred methods: If you mark a method as deferred, the generated code will allow you to
postpone the execution of calls by providing a
std::futurethat can be set by you whenever you decide.
To generate the code in this way, you use a JSON file that may look as follows:
{
"classes": {
"my_package.MyClass": {
"deferredMethods": [
"doingSomethingDeferred",
"doingSomethingDeferredWithoutReturning"
],
"checkedProperties": [
"prop7"
]
}
}
}
Documenting STL files¶
You can add comments to STL files in two main ways:
- Before comments → placed right before the declaration.
- Inline comments → placed at the end of the same line as the declaration.
Only line comments (//) are supported; there is no block-comment syntax.
General rules¶
-
Any object that uses brackets (e.g.,
enumerations,structures,variants,classes)\ → supports before comments for the declaration itself.\ → supports before and inline comments for elements inside the brackets. -
sequences,optionalandquantities→ support before and inline comments.
// A structure to represent a point in 2D space
struct Point
{
// The X coordinate
x: i32,
y: i32 // The Y coordinate
}
Classes¶
In addition to the general documentation rules, classes have additional special documentation for methods and events.
As mentioned in the general rules, they both can have a comment block before their declaration. Within this block, and after the description, the parameters can be documented:
You can add multiple lines of comments, but only one @param per parameter.
Class example:
// Example class demonstrating documentation
class Example
{
// Stores the first name
var firstName : string [static];
// Stores the surname
var surName : string [static];
// An event triggered when something happens
// @param what A description of what happened
// @param count Number of times it occurred
event somethingElseHappened(what: string, count: u32); // Example event
// This is just an example method
// @param example1 This parameter is a string
// You can add more description here of the parameter, but do not repeat @param example1
// @param example2: This parameter is also a string
fn exampleMethod(example1: string, example2: string) -> string;
}
Syntax highlighting¶
Please have a look at the resources/syntax_highlighting/stl folder in the Sen repo.
Importing HLA-FOM-defined types¶
Sometimes it is convenient to reuse HLA-defined types in your STL. You can include HLA FOM XMLs from STL files:
package se;
import "fom/rpr/RPR-Physical_v2.0.xml"
// An Entity model.
struct EntityModel
{
name: string, // Name of this model.
type: rpr.EntityTypeStruct // Type of entity represented by this model.
}
You can see that the rpr.EntityTypeStruct is available. The same happens with all the other types
in the XML.
Keep in mind that Sen (the STL parser) expects the HLA XML files to follow a certain file layout.
- root
- module_1
- file_1.xml
- file_2.xml
- module_2
- file_3.xml
- file_4.xml
...
- module_n
- file_5.xml
- file_6.xml
- mapping_1.xml
- mapping_2.xml
- ...
- mapping_n.xml
This is needed because the XML files may depend on each other, and Sen needs to be able to find them.
Regarding mappings, have a look at the documentation about specifying objects with HLA.
Quick reference¶
One-line form of every declaration kind, for quick recall:
package example;
import "stl/sen/kernel/basic_types.stl"
// Aggregates
enum Status: u8 { active, idle, error }
struct Point { x: f32, y: f32 }
struct Empty;
variant Shape { Point, Line, Circle }
// Type constructors
sequence<u8> ByteStream;
sequence<u8, 1024> BoundedBuffer;
array<f32, 3> Vec3;
optional<string> MaybeName;
quantity<f32, m> Distance [min: 0.0];
alias DeviceId u64;
// behavior
abstract class MyService : extends Base
{
var name : string [static];
var count : u32 [writable, tag: counter];
fn process(input: string) -> bool [const];
event pinged();
}
Examples¶
import "stl/school/person.stl"
package school;
struct ImpartingClass
{
since : TimeStamp,
className : string
}
struct WaitingForStudents
{
since : TimeStamp
}
variant TeacherStatus
{
WaitingForStudents,
ImpartingClass
}
class Teacher: extends Person
{
var status : TeacherStatus [confirmed];
var stressLevel : f32;
fn assignTasks();
event stressLevelPeaked(level : f32);
}
import "stl/school/person.stl"
package school;
struct Sleeping
{
since : TimeStamp,
snortingVolume : f32
}
struct DoingSomething
{
since : TimeStamp,
taskName : string,
difficulty : f32,
progress : f32
}
struct DoingNothing
{
since : TimeStamp
}
variant StudentStatus
{
DoingNothing,
DoingSomething,
Sleeping
}
class Student: extends Person
{
var status : StudentStatus [confirmed];
var focusLevel : f32;
fn startDoingTask(taskName: string, difficulty: f32) -> bool;
event madeSomeNoise(noise: string, volume: f32);
event gotDistracted(reason: string);
}