Esper is a minimal expression-based language that targets C++.
This list is partially a feature matrix of existing features.
- Non-obtrusive syntax, Esper is related to ML
- Readable output based on matching semantics
- Default no-emit mode that delegates output to
clang++
- Expression-level directives for compiler routines
Minimum versions:
- rustup: 1.27.0
- rustc: 1.79.0-nightly
- clang++: 16.0.6+ (release builds available with LLVM installation)
Clone the repository, build with Cargo (cargo build --release
), and run esper --help
. Upstream build is tested on Debian with ELF binaries (target x86_64-pc-linux-gnu
). On Windows, demangling issues may arise unless the prelude is excluded (WSL/MinGW should be fine).
Range expressions require -std=c++20
when compiling the output C++ source. Optional flags are passed to clang++
as raw arguments.
esper <input> -o <output> -- -std=c++20 -Wall -O3
The table below compares Esper source programs to the corresponding C++ output (target is EmitDefault
). In context, a main
function definition is expected since every module is in a separate namespace. Refer to the tests.
Item | Esper | C++ | Description |
---|---|---|---|
Type alias |
type T = int end |
using T = int; |
- |
Parametric type alias |
type T<K> = K end |
template<typename K>
using T = K; |
Type parameters are required when instantiating. |
Reference & pointer types |
type T<K> = &K end
type P<U> = **U end |
template<typename K>
using T = &K;
template<typename U>
using P = **U; |
Type parameters are required when instantiating. |
Variant types (tagged unions) |
type N = | int | float end
type V<T, K> = | T | K end |
using N = std::variant<int, float>;
template<typename T, typename K>
using V = std::variant<T, K>; |
- |
Optional type |
type T = ?int end
type K =
| ?int
| ?bool
end
type U = ?| int | bool end |
using T = std::optional<int>;
using K = std::variant<std::optional<int>, std::optional<bool>>;
using U = std::optional<std::variant<int, bool>>; |
Alias of |
Mapped types |
type M<K, V> = { key: K, value: V } end
type tree<T> = {
value: T,
children: vector<tree<T>>
} end |
template<typename K, typename V>
struct M {
using key = K;
using value = V;
}
template<typename T>
struct tree {
using value = T;
using children = std::vector<tree<T>>;
} |
Represents structural definitions that can be passed as signatures in polymorphic functions. |
Type members |
type P<Q> = Q.key. end |
template<typename Q>
using P = Q::key; |
Overloads the |
Type extensions |
@extend(S, string) type R<S> = S end |
template<typename S>
using R = std::enable_if_t<
std::is_same<
S, std::string>::value
S
>; |
|
Type-level function definition |
type F =
|a: int, b: float| ?int end
end |
using F = std::function<std::optional<int>(int, int)>; |
Return types are parsed as |
Pattern matching |
match n with
| int ->
print("-> scope");
print("int: ", _),
| string -> print("string: ", _),
end |
std::visit([](auto&& _) {
using T = std::decay_t<decltype(_)>;
if constexpr (std::is_same_v<T, int>) {
print("-> scope");
print("int: ", _);
}
if constexpr (std::is_same_v<T, string>) {
print("string: ", _);
}
}, n); |
Non-exhaustive matching, inner values captured as the |
Typed definitions |
let n : int = 0
let p : 0 = 0
let t : | bool | string = true |
int n = 0;
decltype(0) p = 0;
std::variant<bool, std::string> t = true; |
|
Parameterized types (postfix generics) |
let lst : vector<int> = [] |
std::vector<int> lst = {}; |
- |
Typed call expressions (postfix generics) |
let lst = vector<int>() |
auto lst = std::vector<int>(); |
- |
Variable definitions |
let n = 0 |
auto n = 0; |
Initialization of a value is expected. Default type is |
Function definitions |
let _ = || pass end (* since v0.2 *)
let add: int = |a: int, b: int| a + b end
let swap: tuple<int> = |a: int, b: int|
let tmp = a;
a = b;
b = tmp;
[a, b]
end |
auto _() { return; }
int add(int a, int b) { return (a + b); }
std::tuple<int> swap(a: int, b: int) {
auto tmp = a;
a = b;
b = tmp;
return {a, b};
} |
Required return type is the lvalue. Non-inferred parameter types. Last expression is returned. Multiline expressions are delimited with |
Struct definition |
struct A end
struct B
c: float,
d: || c end
end |
class A {};
class B {
public:
float c;
auto d() { return c; }
}; |
All symbols are public without |
Loops |
for a in b [] end
for p in q.r. [] end
for i in 0..10
print(i)
end
for [a, b] in c
print(a, b)
end |
for (auto a : b) {}
for (auto p : q.r) {}
for (auto i : views::iota(0,10)) {
print(i);
}
for (auto [a, b] : c) {
print(a, b);
} |
- |
Value operators (since |
~a
&a
&&a
a gt b
a lt b
a gte b
a lte b
a eq b
a neq b
a and b
a or b
a shl b
a shr b
a band b
a bor b
a xor b
a rotl b
a rotr b |
~a
&a
&&a
(a > b);
(a < b);
(a >= b);
(a <= b);
(a == b);
(a != b);
(a && b);
(a || b);
(a << b);
(a >> b);
(a & b);
(a | b);
(a ^ b);
__builtin_rotateleft32(a, b);
__builtin_rotateright32(a, b); |
- |
Esper is experimental and aims to stay minimal. Matching semantics are not optimized, e.g std::visit
for pattern matching is a known performance bottleneck, exclusively using STL Containers (argv
is cast from a const char**
), all libstdc++
headers are included in the optional prelude, passing by value is exclusive and a wide range of impracticable error handling; with clang++
errors being piped to stdin
, PEG's obscure parsing errors (resolved in v0.2
) and no type-level resolution of expressions (requires a complete type system and call graph).
Copyright © 2024 Elric Neumann. MIT License.