Pacts
mimas is built for embedded use, so Rust’s full trait machinery – generics, associated types, blanket impls, and the constraint solver behind them – is intentionally out of scope. Even so, abstracting over types is sometimes genuinely useful: without any way to do it, every function that wants to handle “anything with this behavior” has to be rewritten per concrete type.
It comes up most at the FFI line. A host project large enough to span several crates often leans on traits at its boundaries, and a mimas script needs some way to interoperate with at least the simplest of those patterns.
A pact fills that role. It’s a named set of method, associated-function, and constant signatures – a contract a type satisfies by writing impl PactName for TheType { ... } and supplying the listed items. Anywhere a type is expected, a pact can stand in, and the checker will accept any value whose type fulfills it.
pact Draw {
fn draw(self);
}
struct Square;
impl Draw for Square {
fn draw(self) { print("a square"); }
}
struct Circle;
impl Draw for Circle {
fn draw(self) { print("a circle"); }
}
fn render_all(items: [Draw]) {
for item in items {
item.draw();
}
}
render_all([Square, Circle]);
A collection literal holding several types settles on the pacts they share, which is how you build “a list of things that implement Draw” without generics. The branches of an if or a match settle the same way. If the types have no pact in common, it’s an ordinary type mismatch.
This only goes one way. A value typed as a pact could be any implementer, so it can’t be used where a specific one is expected:
let shape: Draw = Square;
let square: Square = shape; // compile error: mismatched types
Constants
A pact can require constants as well as methods.
pact Named {
const NAME: str;
}
struct Square;
impl Named for Square {
const NAME = "Square";
}
let n = Square::NAME; // "Square"
A constant can also be read off a value that is only known by its pact. Like a method call, the value’s own impl is picked at runtime:
fn name_of(thing: Named) -> str {
thing.NAME
}
let n = name_of(Square); // "Square"
Read that way it’s no longer a compile-time value, so it can’t be used to define another const.
Default implementations
A pact method can ship with a default body for functions, which an implementer may use as-is or override. The same is true for constants.
pact Greet {
const DEFAULT_GREETING: str = "hello";
fn name(self) -> str;
fn hello(self) -> str {
f"hi, {self.name()}"
}
}
struct Dog { tag: str }
impl Greet for Dog {
fn name(self) -> str { self.tag }
// `hello` is inherited
}
print(Dog { tag = "rex" }.hello()); // "hi, rex"
print(Dog::DEFAULT_GREETING); // "hello" (inherited from the pact)
A default body can call the pact’s other methods through self, including ones the implementer supplies. An impl that defines the method itself overrides the default.
Self
Inside a pact Self is the implementing type. A method that works on two values of the same type spells the second one Self, and each implementer puts its own type there:
pact Plus {
fn plus(self, other: Self) -> Self;
}
struct V { x: int }
impl Plus for V {
fn plus(self, other: V) -> V {
V { x = self.x + other.x }
}
}
struct W { y: int }
impl Plus for W {
fn plus(self, other: W) -> W {
W { y = self.y + other.y }
}
}
V { x = 1 }.plus(V { x = 2 }); // V { x = 3 }
V { x = 1 }.plus(W { y = 2 }); // compile error: expected Self but found W
Self is not the same as writing the pact’s name. other: Plus means any implementer at all; other: Self means the receiver’s own type, and an impl has to name exactly that type – impl Plus for V can’t take other: W. Inside a default body, self has type Self, so it can be passed wherever the pact asks for Self, and also wherever it asks for the pact itself, since a Self is always an implementer.
Reaching pact methods through a value
Without generics, mimas can’t lean on Rust’s T::ITEM syntax to reach an item on a constrained type. This is why dot access reaches associated items – given a value known only by its pact, . is how you get at its methods.
fn announce(thing: Greet) {
print(thing.hello());
}
Reaching every implementer
Pacts offer a method of reaching all of their implementers at runtime, which enables many options for extendability. Since mimas does not have generics, there’s no way to collect “types” and operate upon them. Instead, pacts can invoke their own items across all of their implementers and collect the results into an array. This is done with the “each” accessor (::*::):
pact Enemy {
fn make() -> Self;
}
struct Goblin { hp: int }
struct Rat { hp: int }
impl Enemy for Goblin {
fn make() -> Self {
Self { hp = 5 }
}
}
impl Enemy for Rat {
fn make() -> Self {
Self { hp = 10 }
}
}
let all_enemies: [Enemy] = Enemy::*::make(); // contains a Goblin and a Rat
This also works for collecting the constants on implementers.
pact Mod {
const NAME: str;
}
fn check_mod_present(name: str) -> bool {
name in Mod::*::NAME
}
A Self return comes back as the pact, so Enemy::*::make() is an [Enemy], and a function that returns nothing gives [()]. A pact with no implementers gives an empty array.
Nothing promises which implementer is called first or where its result lands in the array, and the order may change between versions of mimas. If a script needs to tell the results apart, have the function return something that says which implementer it came from.
Only associated functions and constants can be reached this way. A method has no receiver here:
Enemy::*::hp(); // compile error: `hp` can't be called on every implementer of `Enemy`
The IR deconstructs the Pact::*::foo() pattern such that arguments are evaluated once and reused for every call.
The path starts from the pact’s name, which can be imported or written through its module (enemies::Enemy::*::make()). It can’t start from a value of that type, since a value’s type may implement several pacts and the name is what picks one.
Binding multiple pacts
An annotation can require several pacts at once with +:
struct Widget {
item: Named + Greet,
}
To make a multi-pact bound optional or a result, wrap it in parentheses first so the ?/! applies to the whole thing:
struct Widget {
item: (Named + Greet)?,
}
Limitations
Pacts are the youngest part of the language. Each of the following is rejected with a clear error rather than silently misbehaving.
Self::CONSTANT can’t be read in a default body. A default body is shared by every impl, so Self there is no one type, and unlike a value it has nothing to pick an impl’s constant by:
pact Named {
const NAME: str;
fn describe() -> str {
Self::NAME // compile error: pact constant `NAME` can't be reached through `Self`
}
}
A method can read it off self instead (self.NAME), since a value does say which impl it is.
Methods that take Self can’t be called through a pact. Self in a parameter means “the same type as the receiver”, and a value known only by its pact doesn’t say what that is – a.plus(b) with both typed Plus could pair a V with a W. The call is rejected wherever the receiver is only known by its bound, even when the arguments happen to match:
fn combine(a: Plus, b: Plus) -> Plus {
a.plus(b) // compile error: pact method `plus` can't be called through `Plus`
}
Call it on a concrete type instead (fn combine(a: V, b: V) -> V). The rule is per method, not per pact: the pact’s other methods stay callable through the bound, a Self that appears only in the return type is fine (the result comes back as the bound), and inside a default body self.plus(self) works because every Self there is the same type.
Self:: doesn’t resolve inside a default body. A default body is compiled once and shared by every implementer, so Self there is a type that hasn’t been decided yet, and there’s no single constant or associated function for Self:: to reach. Self::CONST and Self::assoc_fn() are both rejected inside one. Lowercase self – fields, methods, parameters – works normally, and Self:: resolves as expected inside an ordinary impl block.
No generics. A pact bound is the only form of abstraction over types. There are no type parameters, associated types, or blanket impls.