21 releases (1 stable)
1.0.0 | Jan 5, 2024 |
---|---|
0.6.1 | Jan 4, 2024 |
0.5.2 | Dec 29, 2023 |
0.5.0 | Nov 24, 2023 |
0.2.6 | Oct 27, 2023 |
#311 in Procedural macros
251 downloads per month
495KB
10K
SLoC
Chandeliers-Lus
Frondend of the Chandeliers suite.
This crate provides the decl
macro that defines a deep embedding of Lustre into Rust.
Usage
In this presentation of the usage of decl
we assume a familiarity with the
Lustre language.
Declaring constants and nodes
A self-contained Lustre program can usually be enclosed in a decl
macro with barely
any change. Here are some examples:
chandeliers_lus::decl! {
const ZERO: int = 0;
const ONE: int = 1;
node fibonacci() returns (n : int);
let
n = ZERO -> ONE -> (pre n + pre pre n);
tel;
node fibsum() returns (s : int);
let
s = fibonacci() + (0 fby s);
tel;
}
chandeliers_lus::decl! {
node selector(left, right : float; switch : bool) returns (out : float);
let
out = if switch then left else right end;
tel;
}
Importing external definitions
Chandeliers does not provide any builtin functions. This is intentional.
Instead a flexible and robust method is available for declaring extern definitions.
Any extern
defined object will be assumed by the static analyzers of Chandeliers
to be defined, and if it is missing at codegen time then Rustc will emit an error.
extern
covers
- declarations from another
decl
block (example below), - nodes from the standard library (see crate
chandeliers-std
) or from another crate, - custom nodes written directly in Candle (more information in
chandeliers-sem
, examples intests/pass/std
and in particulartests/pass/std/rand.rs
).
Using a definition from another decl
block:
chandeliers_lus::decl! {
node counter() returns (n : int);
let n = 0 fby (n + 1); tel;
}
// `decl` is local and cannot know that `counter` exists in another block.
// If we want to use it here, we need to redeclare it as an `extern node`.
chandeliers_lus::decl! {
// any signature inconsistencies will produce type errors.
extern node counter() returns (n : int);
node cumul() returns (n : int);
let n = counter() + (0 fby n); tel;
}
In the same way an
chandeliers_lus::decl! {
extern const PI: float;
...
makes PI
available to the rest of the block.
Using a definition from the standard library:
use chandeliers_std::float_of_int;
chandeliers_lus::decl! {
extern node float_of_int(i : int) returns (f : float);
...
}
How to write a main
function
The decl
macro never inserts a main
, instead one should be defined manually
to interface with the environment.
This is made possible by tha fact that a decl
block makes its definitions
publicly available under the same name.
A main
could look something like this:
use chandeliers_sem::traits::*;
chandeliers_lus::decl! {
node cumul(i : int) returns (s : int);
let s = i + (0 fby s); tel;
}
fn main() {
let mut cumul = cumul::default();
for i in 0..100 {
let s = cumul.step(i.embed()).trusted();
println!("{s}");
}
}
The above features are provided by the traits Step
, Default
, Embed
and Trusted
which are implemented automatically for relevant objects either by the decl
macro itself
or defined generically in chandeliers-sem
.
Nodes produced by decl
manipulate different types of values internally
(isomorphic to Option<T>
), and the traits Embed
and Trusted
produce the
equivalent of map(Some)
and map(Option::unwrap)
on all tuple types.
More specifically, a tuple type (T, U, V)
implements Embed
with output type
(Nillable<T>, Nillable<U>, Nillable<V>)
, and Trusted
produces the reciprocal.
It is always safe to use Embed
, but you should only use Trusted
on values that
are not Nil
. The static analysis performed inside decl
guarantees that a node
whose inputs are all non-Nil
will have non-Nil
outputs, so only the implementation
of extern node
s not produced by a decl
block needs to be verified.
Notable limitations and differences with standard Lustre
For various reasons -- including limited control over Rust syntax, and parser efficiency -- some Lustre programs may not be supported directly and require minor syntax adjustments. Here are some that you may run into if you try to copy-paste Lustre code too eagerly:
node
definitions are required to end with a trailing;
,- tuple unpacking
(x, y, z) = foo()
requires the surrounding(...)
, - the Rust convention is used to determine in which contexts trailing commas are allowed:
1 = (1) <> (1,)
and(1, 2) = (1, 2,)
, - a few Rust reserved keywords (
self
,super
,move
, ...) cannot be used as identifiers, - only Rust-style comments are available, i.e.
//
for line comments and/* ... */
for block comments, f((a, b, c))
is implicitly coerced tof(a, b, c)
allowing trivial composition of functions when the output tuple of one has the same arity as the input tuple of another.
In addition, the following choices have been made with regards to the semantics:
- the delay operator
->
has special associativity rules that makea -> b -> c
(a1, b2, c3, c4, c5, c6, ...) equal to neithera -> (b -> c)
(a1, c2, c3, c4, c5, c6, ...) nor(a -> b) -> c
(a1, c2, c3, c4, c5, c6, ...). - temporal operators
_ -> _
,pre _
,_ fby _
are only available for expressions with the implicit clock of the node. This restriction is lifted by using the annotation#[universal_pre]
which uses a different encoding of temporal operators that is compatible with clocked expressions, but this may come at a slight performance cost.
Dependencies
~0.4–0.8MB
~19K SLoC