Rust lets you say what your data is, exactly, in types. A struct says "all of these together". An enum says "exactly one of these". Option says "maybe nothing", and Result says "this or a reason it failed". Gestalt leans on this hard: a refusal is a value with a class and a code, and a missing fact is a hole with a reason. Neither is ever false, 0 or a crash.
The idea #
Structs: fields together #
struct Refusal {
class: Class,
code: &'static str,
next: Option<Next>,
}
struct Digest(pub [u8; 32]); // a "newtype": one field, a type of its ownA newtype costs nothing at run time. But a Digest can no longer be passed where some other 32 bytes are wanted, like a key. Read the field with .0.
Methods go in an impl block. &self borrows the value; &mut self may change it; self consumes it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] asks the compiler to write the obvious code: printing, copying, comparing.
Enums: exactly one of these #
enum Value {
U(u64), // a variant can carry data
T(String),
Bool(bool),
Null, // or none
}A Value is one of these and nothing else. There is no "null pointer" and no "unknown kind".
match: every case, or it won't build #
fn kind(v: &Value) -> &'static str {
match v {
Value::U(_) => "unsigned",
Value::T(_) => "text",
Value::Bool(_) => "bool",
Value::Null => "null",
}
}A match must cover every variant. Add a variant to the enum, and the compiler lists every match that has to learn about it. So a catch-all _ => arm should be a choice, not a habit. Patterns can take data out (Value::U(n) => n), match ranges (0..=23) and match pairs ((Some(a), Some(b))).
Option: maybe nothing #
enum Option<T> { Some(T), None }Option<u64> is not a number. You can't add it to anything until you say what None means. if let Some(n) = x { ... } and let Some(n) = x else { return ... }; take the value out or handle its absence.
Result and ?: this, or why not #
enum Result<T, E> { Ok(T), Err(E) }
fn byte(&mut self) -> Result<u8, CborError> {
let x = *self.bytes.get(self.at).ok_or(CborError::Truncated)?;
self.at += 1;
Ok(x)
}? means "if this is an Err, return it now; otherwise give me the value". A chain of steps that can each fail reads like a straight line. The first failure ends it.
Three converters you will use all the time:
.ok_or(e)turns anOptioninto aResult:NonebecomesErr(e)..map_err(|_| e)swaps aResult's error for yours..checked_add(b)returnsNoneinstead of overflowing.
.unwrap() and .expect("...") take the value or panic. They are fine in tests and drills, and never right for bytes that came from a stranger.
Why Gestalt cares #
- A refusal is a value. When admit refuses an entry, it returns a
Refusal: a class, a stable code, what to do next, and what it was about. The caller can read it, show it, and match on it. Nothing is thrown and nothing crashes. - A hole is never false. When derive can't answer, because an input is missing, a rule is absent or a sum overflows, it returns a hole with a
Reason. Gestalt's fixed rule: a missing fact is never false, and a withheld one is never "no".OptionandResultmake that rule something the compiler enforces. - Closed lists. Refusal classes, hole reasons and CBOR errors are enums, closed lists with stable codes that the spec's vectors name. A new reason can't slip in unnoticed: every
matchon the list must learn it. - Never crash on strangers' bytes. A node decodes whatever arrives. The decoder returns
Errfor every defect, so bad input can't take the node down. Exercise 7 checks your decoder against every one- and two-byte input there is.
In Gestalt's code #
The codec's one data item. Your Value is this without arrays and maps.
From engine/kernel/codec/src/cbor.rs:39:
pub enum Value {
/// An unsigned integer.
U(u64),
/// A negative integer, `-1 - n`.
N(u64),
/// A byte string.
B(Vec<u8>),
/// A text string.
T(String),
/// An array.
A(Vec<Value>),
/// A map with unsigned-integer keys.
M(Map),
/// `true` or `false`.
Bool(bool),
/// `null`.
Null,
}decode is your exercise 7, with the reader's ? doing the work. ok_or and ? together in one line is the engine's own byte (cbor.rs:367).
From engine/kernel/codec/src/cbor.rs:341:
pub fn decode(bytes: &[u8]) -> Result<Value, CborError> {
let mut r = Reader { b: bytes, at: 0 };
let v = r.item()?;
if r.at != bytes.len() {
return Err(CborError::TrailingBytes);
}
Ok(v)
}A refusal: a struct of enums and options. next is an Option because not every refusal has a next step.
From engine/kernel/admit/src/refusal.rs:54:
pub struct Refusal {
/// The class.
pub class: Class,
/// The stable code (the conformance cases and the node name it).
pub code: &'static str,
/// What the caller can do next.
pub next: Option<Next>,
/// The entry, standing, float or module the refusal is about.
pub about: Option<Digest>,
// ...A hole, and the arithmetic that makes one. Derive computes in i128 and checks every step. Your total in exercise 8 is the same idea.
From engine/kernel/derive/src/hole.rs:1 and engine/kernel/derive/src/num.rs:57:
//! Holes: questions not yet answered, with their reason (ANSWER-17).
//! ... A missing fact is never false and a withheld one is never "no":
//! both are holes (fixed truth 8, PA175).
pub fn add(a: i128, b: i128) -> Result<i128, Reason> {
a.checked_add(b).ok_or(Reason::Overflow)
}Exercise #
cd ~/Documents/gestalt-learn
./ladder 3Open rungs/03-types/src/lib.rs. The helpers from rungs 1 and 2 are given, written as the engine writes them. Eight exercises take you from a newtype to a full canonical decoder. One test feeds your decoder all 65,792 inputs of one and two bytes. It must never panic, and whatever it accepts must encode back to the same bytes. That is the codec's own promise.
Then three drills:
./ladder drill 3 every_case
./ladder drill 3 question_mark
./ladder drill 3 unknown_is_not_zeroStuck? rungs/03-types/solution/ has one way to do it, and ./ladder solution 3 runs the tests against it.
Check yourself #
- Why is a
matchwithout a catch-all safer when the enum grows? - What does
?do, and where can you use it? - Why is an unknown amount a hole and not 0?
- Why must the decoder return
Errrather than panic?