Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Skeleton Is the Spec

A prose spec constrains the agent weakly. A skeleton of types and signatures constrains it mechanically:

/// Decodes raw frames into engineering-unit signal values.
pub struct SignalDecoder { /* fields to be decided */ }

impl SignalDecoder {
    /// Parse a decoder from a signal database. Rejects malformed input.
    pub fn from_dbc(src: &str) -> Result<Self, DbcParseError> { todo!() }

    /// Decode one frame. A frame for an unknown message is not an error.
    pub fn decode(&self, frame: &Frame) -> Option<DecodedSignals> { todo!() }
}

These nine lines already rule out several kinds of generated code:

  • A decoder built from a partially parsed database. Result forces the question of what happens on bad input.
  • Treating an unknown frame as an error or a crash. Option declares it a normal outcome.
  • Mutating the decoder while decoding. &self forbids it, which also keeps decoding shareable across threads.
  • An improvised error strategy. DbcParseError is a type you defined.

The workflow is a loop, and the division of labor in it doesn’t change: bodies are generated, skeletons are written by hand. A skeleton is a page of code at most, so typing it’s the cheap part. The decisions it records are the work, and a generated skeleton is a set of decisions nobody made.

  1. You write the skeleton.
  2. The agent fills in the bodies.
  3. You review the bodies.
  4. When a review finding recurs, you change the skeleton so that the whole class of defect no longer compiles, and regenerate.

Step 4, concretely: a review that keeps finding unwrap on lookups changes the signature the bodies are generated against.

// before: bodies keep unwrapping the miss
pub fn scale_for(&self, id: CanId) -> f64 { todo!() }

// after: the miss is in the type; unwrapping it is now visible
pub fn scale_for(&self, id: CanId) -> Option<f64> { todo!() }