name.rs

24.3 kB · rust · 650 lines

1use crate::core::error::{value_error, Error, Result};2use crate::gen::recipe::{Design, Group, Source, Tile as Recipe};3use crate::math::name::{kind, Named};4use serde::{Deserialize, Serialize};56kind!("tile");78const CODES_2D: [(Design, u128); 10] = [9    (Design::Carpet, 7),10    (Design::Net, 14),11    (Design::Htree, 3),12    (Design::Vtree, 5),13    (Design::Void, 9),14    (Design::Point, 8),15    (Design::Dust, 1),16    (Design::Hline, 12),17    (Design::Vline, 10),18    (Design::Star, 6),19];2021const TOTAL_2D: u128 = 16;2223/// Returns the plane's bang code of a classic design, or None for one outside the plane.24pub fn classic_code(design: Design) -> Option<u128> {25    CODES_2D26        .iter()27        .find(|&&(d, _)| d == design)28        .map(|&(_, code)| code)29}3031fn code_of(source: Source) -> Result<u128> {32    match source {33        Source::Classic(design) => match classic_code(design) {34            Some(code) => Ok(code),35            None => value_error(format!(36                "design {} has no code in the plane.",37                design.name()38            )),39        },40        Source::Code(code) => Ok(code),41    }42}4344fn slot<T: Copy>(values: &[T], what: &str) -> Result<T> {45    match values.first() {46        Some(&value) => Ok(value),47        None => value_error(format!("a recipe with no {what} has no name.")),48    }49}5051fn is_false(flag: &bool) -> bool {52    !flag53}5455/// One value for every slot of a tile, or one value per slot.56#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]57#[serde(untagged)]58pub enum Slots {59    /// The one value the slots share.60    One(usize),61    /// One value per slot, in slot order.62    Each(Vec<usize>),63}6465impl Slots {66    fn is_still(&self) -> bool {67        match self {68            Slots::One(turn) => *turn == 0,69            Slots::Each(turns) => turns.iter().all(|&turn| turn == 0),70        }71    }72    fn one(&self, what: &str) -> Result<usize> {73        match self {74            Slots::One(value) => Ok(*value),75            Slots::Each(_) => value_error(format!("a one-slot tile wants one {what}, not a list.")),76        }77    }78    fn each(&self, count: usize, what: &str) -> Result<Vec<usize>> {79        match self {80            Slots::One(0) if what == "turn" => Ok(vec![0; count]),81            Slots::One(_) => value_error(format!("a {count}-slot tile wants a {what} per slot.")),82            Slots::Each(values) if values.len() == count => Ok(values.clone()),83            Slots::Each(values) => value_error(format!(84                "a {count}-slot tile wants {count} {what}s, not {}.",85                values.len()86            )),87        }88    }89}9091impl Default for Slots {92    fn default() -> Slots {93        Slots::One(0)94    }95}9697/// A tile recipe folded to its one canonical object.98///99/// The key that carries the codes says the group: `code` is one design flat or, with `level`, raised100/// to a power; `magic` is a list of letters; `special` is one mask code over a factor; `mosaic` is101/// three codes behind a tree mask. Classics fold to their codes, a lone anti folds into `invert`,102/// and a level of one folds away, so aliases that draw one picture share one name.103///104/// ```105/// use mrlyrs::gen::name::Tile;106/// use mrlyrs::math::name::Named;107/// let carpet = Tile::from_json(r#"{"kind":"tile","code":7,"side":3,"level":2}"#).unwrap();108/// assert_eq!(carpet.recipe().unwrap().width, 9);109/// assert_eq!(Tile::of(&carpet.recipe().unwrap()).unwrap(), carpet);110/// ```111#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]112#[serde(deny_unknown_fields)]113pub struct Tile {114    /// The kind word.115    pub kind: Kind,116    /// The one design of a flat or fractal tile.117    #[serde(default, skip_serializing_if = "Option::is_none")]118    pub code: Option<u128>,119    /// The mask code of a special tile.120    #[serde(default, skip_serializing_if = "Option::is_none")]121    pub special: Option<u128>,122    /// The letters of a magic tile, first letter outermost.123    #[serde(default, skip_serializing_if = "Vec::is_empty")]124    pub magic: Vec<u128>,125    /// The three codes of a mosaic tile.126    #[serde(default, skip_serializing_if = "Vec::is_empty")]127    pub mosaic: Vec<u128>,128    /// The side of the mask of a special or mosaic tile.129    #[serde(default, skip_serializing_if = "Option::is_none")]130    pub factor: Option<usize>,131    /// The side each slot renders at, one per letter for a magic tile.132    pub side: Slots,133    /// The power a fractal tile is raised to, absent at one.134    #[serde(default, skip_serializing_if = "Option::is_none")]135    pub level: Option<usize>,136    /// The quarter turns of each slot, absent when nothing turns.137    #[serde(default, skip_serializing_if = "Slots::is_still")]138    pub turn: Slots,139    /// Whether each slot swaps fill and void, absent when none does.140    #[serde(default, skip_serializing_if = "Vec::is_empty")]141    pub anti: Vec<bool>,142    /// Whether a special tile flips its mask.143    #[serde(default, skip_serializing_if = "is_false")]144    pub flip: bool,145    /// Whether the finished tile inverts.146    #[serde(default, skip_serializing_if = "is_false")]147    pub invert: bool,148}149150fn blank() -> Tile {151    Tile {152        kind: Kind,153        code: None,154        special: None,155        magic: Vec::new(),156        mosaic: Vec::new(),157        factor: None,158        side: Slots::One(0),159        level: None,160        turn: Slots::One(0),161        anti: Vec::new(),162        flip: false,163        invert: false,164    }165}166167fn turns(recipe: &Recipe) -> Slots {168    Slots::Each(recipe.rotations.iter().map(|r| r % 4).collect())169}170171fn plane(code: u128) -> Result<u128> {172    if code >= TOTAL_2D {173        return value_error(format!("code {code} is not in the plane (0..15)."));174    }175    Ok(code)176}177178impl Tile {179    /// Folds a recipe to its name.180    ///181    /// # Errors182    ///183    /// Errs when a source carries no code, or when the recipe's slot lists are ragged.184    pub fn of(recipe: &Recipe) -> Result<Tile> {185        let codes = recipe186            .sources187            .iter()188            .map(|&s| code_of(s))189            .collect::<Result<Vec<u128>>>()?;190        let mut name = blank();191        name.invert = recipe.invert;192        match recipe.group {193            Group::General | Group::Fractal => {194                name.code = Some(slot(&codes, "source")?);195                name.side = Slots::One(slot(&recipe.numbers, "number")?);196                if recipe.group == Group::Fractal {197                    let level = slot(&recipe.levels, "level")?;198                    name.level = (level != 1).then_some(level);199                }200                name.turn = Slots::One(slot(&recipe.rotations, "rotation")? % 4);201                name.invert = slot(&recipe.anti, "anti flag")? ^ recipe.invert;202            }203            Group::Magic => {204                name.magic = codes;205                name.side = Slots::Each(recipe.numbers.clone());206                name.turn = turns(recipe);207                name.anti = recipe.anti.clone();208            }209            Group::Special => {210                name.special = Some(slot(&codes, "source")?);211                name.factor = Some(recipe.factor);212                name.side = Slots::One(slot(&recipe.numbers, "number")?);213                name.turn = Slots::One(slot(&recipe.rotations, "rotation")? % 4);214                name.flip = recipe.flip;215            }216            Group::Mosaic => {217                name.mosaic = codes;218                name.factor = Some(recipe.factor);219                name.side = Slots::One(slot(&recipe.numbers, "number")?);220                name.turn = turns(recipe);221                name.anti = recipe.anti.clone();222            }223        }224        Ok(name.fold())225    }226    fn fold(mut self) -> Tile {227        if self.turn.is_still() {228            self.turn = Slots::One(0);229        }230        if self.anti.iter().all(|&a| !a) {231            self.anti.clear();232        }233        self234    }235    fn group(&self) -> Result<Group> {236        let carried = [237            (self.code.is_some(), Group::General),238            (self.special.is_some(), Group::Special),239            (!self.magic.is_empty(), Group::Magic),240            (!self.mosaic.is_empty(), Group::Mosaic),241        ];242        let mut groups = carried243            .iter()244            .filter(|(held, _)| *held)245            .map(|&(_, group)| group);246        let (Some(group), None) = (groups.next(), groups.next()) else {247            return value_error("a tile carries exactly one of code, special, magic or mosaic.");248        };249        if group == Group::General && self.level.is_some_and(|level| level != 1) {250            return Ok(Group::Fractal);251        }252        if group != Group::General && self.level.is_some() {253            return value_error("level is fractal only.");254        }255        Ok(group)256    }257    fn anti(&self, count: usize) -> Result<Vec<bool>> {258        match self.anti.len() {259            0 => Ok(vec![false; count]),260            n if n == count => Ok(self.anti.clone()),261            n => value_error(format!(262                "a {count}-slot tile wants {count} anti flags, not {n}."263            )),264        }265    }266    fn lone(&self, count: usize) -> Result<()> {267        if !self.anti.is_empty() {268            return value_error("anti folds into invert on a one-slot tile.");269        }270        if count == 1 && self.factor.is_some() {271            return value_error("factor is special or mosaic only.");272        }273        Ok(())274    }275    /// Builds the recipe the name folds, resized and checked.276    ///277    /// # Errors278    ///279    /// Errs when the keys do not say one group, a code is not in the plane, or the check fails.280    pub fn recipe(&self) -> Result<Recipe> {281        let group = self.group()?;282        let mut recipe = Recipe::new(group);283        recipe.invert = self.invert;284        match group {285            Group::General | Group::Fractal => {286                self.lone(1)?;287                if self.flip {288                    return value_error("flip is special only.");289                }290                recipe.sources = vec![Source::Code(plane(self.code.expect("a code"))?)];291                recipe.numbers = vec![self.side.one("side")?];292                recipe.levels = vec![self.level.unwrap_or(1)];293                recipe.rotations = vec![self.turn.one("turn")?];294                recipe.anti = vec![false];295            }296            Group::Magic => {297                let count = self.magic.len();298                if self.flip || self.factor.is_some() {299                    return value_error("a magic tile carries no flip or factor.");300                }301                recipe.sources = self302                    .magic303                    .iter()304                    .map(|&code| Ok(Source::Code(plane(code)?)))305                    .collect::<Result<Vec<Source>>>()?;306                recipe.numbers = self.side.each(count, "side")?;307                recipe.levels = vec![1; count];308                recipe.rotations = self.turn.each(count, "turn")?;309                recipe.anti = self.anti(count)?;310            }311            Group::Special => {312                if !self.anti.is_empty() {313                    return value_error("anti is dead on a special tile.");314                }315                let Some(factor) = self.factor else {316                    return value_error("a special tile wants its factor.");317                };318                recipe.sources = vec![Source::Code(plane(self.special.expect("a mask code"))?)];319                recipe.factor = factor;320                recipe.numbers = vec![self.side.one("side")?];321                recipe.levels = vec![1];322                recipe.rotations = vec![self.turn.one("turn")?];323                recipe.anti = vec![false];324                recipe.flip = self.flip;325            }326            Group::Mosaic => {327                if self.flip {328                    return value_error("flip is special only.");329                }330                let Some(factor) = self.factor else {331                    return value_error("a mosaic tile wants its factor.");332                };333                recipe.sources = self334                    .mosaic335                    .iter()336                    .map(|&code| Ok(Source::Code(plane(code)?)))337                    .collect::<Result<Vec<Source>>>()?;338                recipe.factor = factor;339                recipe.numbers = vec![self.side.one("side")?; 3];340                recipe.levels = vec![1; 3];341                recipe.rotations = self.turn.each(3, "turn")?;342                recipe.anti = self.anti(3)?;343            }344        }345        recipe.resize();346        recipe347            .check()348            .map_err(|note| Error::Value(format!("tile fails its check: {note}.")))?;349        Ok(recipe)350    }351}352353impl Named for Tile {354    const KIND: &'static str = "tile";355    const LISTS: &'static [&'static str] = &["magic", "mosaic", "anti"];356    fn checked(self) -> Result<Tile> {357        Tile::of(&self.recipe()?)358    }359}360361#[cfg(test)]362mod tests {363    use super::*;364    use crate::core::rng::Rng;365    use crate::gen::build::{build_2d, create_2d, Config2d};366    use crate::gen::recipe::{Catalog, Parity};367    use crate::math::bang::Code;368    use crate::math::two::designs;369370    const CARPET: &str = r#"{"kind":"tile","code":7,"side":3,"level":2}"#;371    const GENERAL: &str = r#"{"kind":"tile","code":3,"side":5,"turn":1,"invert":true}"#;372    const MAGIC: &str = r#"{"kind":"tile","magic":[7,14],"side":[3,5],"turn":[0,2],"anti":[false,true],"invert":true}"#;373    const SPECIAL: &str = r#"{"kind":"tile","special":5,"factor":3,"side":5,"flip":true}"#;374    const MOSAIC: &str = r#"{"kind":"tile","mosaic":[7,14,5],"factor":3,"side":3,"turn":[0,1,0],"anti":[false,false,true],"invert":true}"#;375376    fn built(recipe: &Recipe) -> crate::math::two::Cell2d {377        build_2d(recipe).unwrap()378    }379    fn fractal(design: Design) -> Recipe {380        let mut recipe = Recipe::new(Group::Fractal);381        recipe.sources = vec![Source::Classic(design)];382        recipe.numbers = vec![3];383        recipe.levels = vec![2];384        recipe.rotations = vec![0];385        recipe.anti = vec![false];386        recipe.resize();387        recipe388    }389390    #[test]391    fn classic_codes_match_their_renders() {392        for (design, code) in CODES_2D {393            let by_name = designs::create(Code::from(code), 3, 1, 0, 2).unwrap();394            let by_classic = match design {395                Design::Carpet => designs::carpet(3, 1).unwrap(),396                Design::Net => designs::net(3, 1).unwrap(),397                Design::Htree => designs::htree(3, 1).unwrap(),398                Design::Vtree => designs::vtree(3, 1).unwrap(),399                Design::Void => designs::void(3, 1).unwrap(),400                Design::Point => designs::point(3, 1).unwrap(),401                Design::Dust => designs::dust(3, 1).unwrap(),402                Design::Hline => designs::hline(3, 1).unwrap(),403                Design::Vline => designs::vline(3, 1).unwrap(),404                Design::Star => designs::star(3, 1).unwrap(),405                _ => unreachable!(),406            };407            assert_eq!(by_name, by_classic, "{}", design.name());408        }409    }410    #[test]411    fn example_names_hold_verbatim() {412        assert_eq!(413            Tile::of(&fractal(Design::Carpet)).unwrap().to_json(),414            CARPET415        );416        let mut general = Recipe::new(Group::General);417        general.sources = vec![Source::Code(3)];418        general.numbers = vec![5];419        general.levels = vec![1];420        general.rotations = vec![1];421        general.anti = vec![false];422        general.invert = true;423        general.resize();424        assert_eq!(Tile::of(&general).unwrap().to_json(), GENERAL);425        let mut magic = Recipe::new(Group::Magic);426        magic.sources = vec![Source::Classic(Design::Carpet), Source::Code(14)];427        magic.numbers = vec![3, 5];428        magic.levels = vec![1, 1];429        magic.rotations = vec![0, 2];430        magic.anti = vec![false, true];431        magic.invert = true;432        magic.resize();433        assert_eq!(Tile::of(&magic).unwrap().to_json(), MAGIC);434        let mut special = Recipe::new(Group::Special);435        special.sources = vec![Source::Classic(Design::Vtree)];436        special.factor = 3;437        special.numbers = vec![5];438        special.levels = vec![1];439        special.rotations = vec![0];440        special.anti = vec![true];441        special.flip = true;442        special.resize();443        assert_eq!(Tile::of(&special).unwrap().to_json(), SPECIAL);444        let mut mosaic = Recipe::new(Group::Mosaic);445        mosaic.sources = vec![Source::Code(7), Source::Code(14), Source::Code(5)];446        mosaic.factor = 3;447        mosaic.numbers = vec![3, 3, 3];448        mosaic.levels = vec![1, 1, 1];449        mosaic.rotations = vec![0, 1, 0];450        mosaic.anti = vec![false, false, true];451        mosaic.invert = true;452        mosaic.resize();453        assert_eq!(Tile::of(&mosaic).unwrap().to_json(), MOSAIC);454    }455    #[test]456    fn the_views_hold_verbatim() {457        let magic = Tile::from_json(MAGIC).unwrap();458        assert_eq!(459            magic.to_url().unwrap(),460            "/tile?magic=7,14&side=3,5&turn=0,2&anti=false,true&invert=true"461        );462        assert_eq!(463            magic.to_file().unwrap(),464            "tile_magic=[7,14]_side=[3,5]_turn=[0,2]_anti=[false,true]_invert=true"465        );466        assert_eq!(467            magic.to_mrly().unwrap(),468            "tile magic [7 14], side [3 5], turn [0 2], anti [false true], invert"469        );470        let carpet = Tile::from_json(CARPET).unwrap();471        assert_eq!(carpet.to_url().unwrap(), "/tile?code=7&side=3&level=2");472        assert_eq!(carpet.to_file().unwrap(), "tile_code=7_side=3_level=2");473        assert_eq!(carpet.to_mrly().unwrap(), "tile code 7, side 3, level 2");474        assert_eq!(475            Tile::from_json(SPECIAL).unwrap().to_mrly().unwrap(),476            "tile special 5, factor 3, side 5, flip"477        );478    }479    #[test]480    fn parsed_tiles_pass_check_and_build() {481        for name in [CARPET, GENERAL, MAGIC, SPECIAL, MOSAIC] {482            let tile = Tile::from_json(name).unwrap();483            let recipe = tile.recipe().unwrap();484            assert!(recipe.check().is_ok());485            assert_eq!(tile.to_json(), name);486            assert_eq!(487                Tile::from_url(&tile.to_url().unwrap()).unwrap(),488                tile,489                "{name}"490            );491            assert_eq!(492                Tile::from_file(&tile.to_file().unwrap()).unwrap(),493                tile,494                "{name}"495            );496            let cell = built(&recipe);497            assert_eq!(cell.width(), recipe.width);498        }499    }500    #[test]501    fn a_level_of_one_folds_to_the_flat_tile() {502        let mut flat = fractal(Design::Carpet);503        flat.levels = vec![1];504        flat.resize();505        let name = Tile::of(&flat).unwrap();506        assert_eq!(name.to_json(), r#"{"kind":"tile","code":7,"side":3}"#);507        let spelt = Tile::from_json(r#"{"kind":"tile","code":7,"side":3,"level":1}"#).unwrap();508        assert_eq!(spelt, name);509        let recipe = spelt.recipe().unwrap();510        assert_eq!(recipe.group, Group::General);511        assert_eq!(built(&recipe), built(&flat));512    }513    #[test]514    fn anti_invert_pairs_share_one_name_and_one_picture() {515        let mut plain = fractal(Design::Carpet);516        let mut folded = plain.clone();517        folded.anti = vec![true];518        folded.invert = true;519        assert_eq!(Tile::of(&plain).unwrap(), Tile::of(&folded).unwrap());520        assert_eq!(built(&plain), built(&folded));521        plain.invert = true;522        let mut alias = plain.clone();523        alias.anti = vec![true];524        alias.invert = false;525        assert_eq!(Tile::of(&plain).unwrap(), Tile::of(&alias).unwrap());526        assert_eq!(built(&plain), built(&alias));527        assert_eq!(528            Tile::of(&plain).unwrap().to_json(),529            r#"{"kind":"tile","code":7,"side":3,"level":2,"invert":true}"#530        );531    }532    #[test]533    fn classics_and_codes_share_one_name() {534        let by_classic = fractal(Design::Net);535        let mut by_code = by_classic.clone();536        by_code.sources = vec![Source::Code(14)];537        assert_eq!(Tile::of(&by_classic).unwrap(), Tile::of(&by_code).unwrap());538        assert_eq!(built(&by_classic), built(&by_code));539    }540    #[test]541    fn dead_special_anti_folds_away() {542        let mut special = Recipe::new(Group::Special);543        special.sources = vec![Source::Code(5)];544        special.factor = 3;545        special.numbers = vec![3];546        special.levels = vec![1];547        special.rotations = vec![0];548        special.anti = vec![true];549        special.resize();550        let parsed = Tile::from_json(&Tile::of(&special).unwrap().to_json())551            .unwrap()552            .recipe()553            .unwrap();554        assert_eq!(parsed.anti, vec![false]);555        assert_eq!(built(&special), built(&parsed));556    }557    #[test]558    fn a_spelt_default_folds_to_the_canonical_string() {559        let spelt = r#"{"kind":"tile","side":[3,5],"magic":[7,14],"turn":[0,0],"anti":[false,false],"invert":false}"#;560        let tile = Tile::from_json(spelt).unwrap();561        assert_eq!(562            tile.to_json(),563            r#"{"kind":"tile","magic":[7,14],"side":[3,5]}"#564        );565        assert_eq!(tile.turn, Slots::One(0));566        assert!(tile.anti.is_empty());567    }568    #[test]569    fn refuses_a_name_or_a_recipe_that_cannot_draw() {570        for bad in [571            r#"{"kind":"tile","code":7,"side":3,"turn":4}"#,572            r#"{"kind":"tile","code":16,"side":3}"#,573            r#"{"kind":"tile","code":7,"side":99}"#,574            r#"{"kind":"tile","code":7,"side":3,"flip":true}"#,575            r#"{"kind":"tile","code":7,"side":3,"anti":[true]}"#,576            r#"{"kind":"tile","code":7,"side":3,"factor":3}"#,577            r#"{"kind":"tile","code":7,"side":[3]}"#,578            r#"{"kind":"tile","code":7,"side":3,"level":7}"#,579            r#"{"kind":"tile","code":7,"magic":[7,14],"side":3}"#,580            r#"{"kind":"tile","side":3}"#,581            r#"{"kind":"tile","sparkle":7,"side":3}"#,582            r#"{"kind":"tile","magic":[7],"side":[3]}"#,583            r#"{"kind":"tile","magic":[7,14],"side":3}"#,584            r#"{"kind":"tile","magic":[7,14],"side":[3,5],"anti":[true]}"#,585            r#"{"kind":"tile","magic":[7,14],"side":[3,5],"level":2}"#,586            r#"{"kind":"tile","mosaic":[7,14,5],"factor":3,"side":[3,3,3]}"#,587            r#"{"kind":"tile","mosaic":[7,14],"factor":3,"side":3}"#,588            r#"{"kind":"tile","mosaic":[7,14,5],"side":3}"#,589            r#"{"kind":"tile","special":5,"side":5}"#,590            r#"{"kind":"tile","special":5,"factor":3,"side":5,"anti":[true]}"#,591            r#"{"kind":"bang","dim":2,"code":7}"#,592            "tile code 7, side 3, level 2",593            "tile_code=7_side=3_level=2",594        ] {595            assert!(Tile::from_json(bad).is_err(), "{bad}");596        }597        assert!(Tile::of(&Recipe::new(Group::General)).is_err());598        let mut cubic = fractal(Design::Xtree);599        cubic.resize();600        assert!(classic_code(Design::Xtree).is_none());601        assert!(Tile::of(&cubic).is_err());602    }603    #[test]604    fn seeded_tiles_round_trip() {605        let config = Config2d {606            catalog: Catalog::Universe,607            min_size: 2,608            max_size: 64,609            parity: Parity::Both,610            ..Config2d::default()611        };612        for s in 0..300 {613            let mut rng = Rng::new(s);614            let recipe = create_2d(&config, &mut rng).unwrap();615            let name = Tile::of(&recipe).unwrap();616            let text = name.to_json();617            let parsed = Tile::from_json(&text).unwrap();618            assert_eq!(parsed.to_json(), text, "seed {s}");619            assert_eq!(620                Tile::from_url(&name.to_url().unwrap()).unwrap(),621                name,622                "seed {s}"623            );624            assert_eq!(625                Tile::from_file(&name.to_file().unwrap()).unwrap(),626                name,627                "seed {s}"628            );629            let back = parsed.recipe().unwrap();630            assert!(back.check().is_ok(), "seed {s}");631            assert_eq!(built(&back), built(&recipe), "seed {s}");632        }633    }634    #[test]635    fn seeded_classic_tiles_round_trip() {636        let config = Config2d {637            min_size: 2,638            max_size: 64,639            parity: Parity::Both,640            ..Config2d::default()641        };642        for s in 0..300 {643            let mut rng = Rng::new(s);644            let recipe = create_2d(&config, &mut rng).unwrap();645            let text = Tile::of(&recipe).unwrap().to_json();646            let parsed = Tile::from_json(&text).unwrap();647            assert_eq!(built(&parsed.recipe().unwrap()), built(&recipe), "seed {s}");648        }649    }650}