notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

parser.rs (17699B)


      1 use crate::ast::*;
      2 use crate::tokenizer::{tokenize, Token};
      3 use std::fmt;
      4 
      5 #[derive(Debug)]
      6 pub struct ParseError {
      7     pub msg: String,
      8 }
      9 
     10 impl fmt::Display for ParseError {
     11     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     12         write!(f, "parse error: {}", self.msg)
     13     }
     14 }
     15 
     16 impl std::error::Error for ParseError {}
     17 
     18 /// Parse an s-expression string into a Space.
     19 pub fn parse(input: &str) -> Result<Space, ParseError> {
     20     let tokens = tokenize(input).map_err(|e| ParseError {
     21         msg: format!("tokenization failed: {}", e),
     22     })?;
     23 
     24     let mut parser = Parser {
     25         tokens,
     26         pos: 0,
     27         cells: Vec::new(),
     28         attributes: Vec::new(),
     29         child_ids: Vec::new(),
     30     };
     31 
     32     let root = parser.parse_cell().ok_or_else(|| ParseError {
     33         msg: "failed to parse root cell".into(),
     34     })?;
     35 
     36     Ok(Space {
     37         cells: parser.cells,
     38         attributes: parser.attributes,
     39         child_ids: parser.child_ids,
     40         root,
     41     })
     42 }
     43 
     44 struct Parser<'a> {
     45     tokens: Vec<Token<'a>>,
     46     pos: usize,
     47     cells: Vec<Cell>,
     48     attributes: Vec<Attribute>,
     49     child_ids: Vec<CellId>,
     50 }
     51 
     52 #[derive(Clone)]
     53 struct Checkpoint {
     54     pos: usize,
     55     cells_len: usize,
     56     attrs_len: usize,
     57     child_ids_len: usize,
     58 }
     59 
     60 impl<'a> Parser<'a> {
     61     fn checkpoint(&self) -> Checkpoint {
     62         Checkpoint {
     63             pos: self.pos,
     64             cells_len: self.cells.len(),
     65             attrs_len: self.attributes.len(),
     66             child_ids_len: self.child_ids.len(),
     67         }
     68     }
     69 
     70     fn restore(&mut self, cp: Checkpoint) {
     71         self.pos = cp.pos;
     72         self.cells.truncate(cp.cells_len);
     73         self.attributes.truncate(cp.attrs_len);
     74         self.child_ids.truncate(cp.child_ids_len);
     75     }
     76 
     77     fn peek(&self) -> Option<&Token<'a>> {
     78         self.tokens.get(self.pos)
     79     }
     80 
     81     fn eat_open(&mut self) -> bool {
     82         if matches!(self.peek(), Some(Token::Open)) {
     83             self.pos += 1;
     84             true
     85         } else {
     86             false
     87         }
     88     }
     89 
     90     fn eat_close(&mut self) -> bool {
     91         if matches!(self.peek(), Some(Token::Close)) {
     92             self.pos += 1;
     93             true
     94         } else {
     95             false
     96         }
     97     }
     98 
     99     fn eat_symbol_match(&mut self, expected: &str) -> bool {
    100         if let Some(Token::Symbol(s)) = self.peek() {
    101             if *s == expected {
    102                 self.pos += 1;
    103                 return true;
    104             }
    105         }
    106         false
    107     }
    108 
    109     fn eat_symbol(&mut self) -> Option<&'a str> {
    110         if let Some(Token::Symbol(s)) = self.peek() {
    111             let s = *s;
    112             self.pos += 1;
    113             Some(s)
    114         } else {
    115             None
    116         }
    117     }
    118 
    119     fn eat_string(&mut self) -> Option<&'a str> {
    120         if let Some(Token::Str(s)) = self.peek() {
    121             let s = *s;
    122             self.pos += 1;
    123             Some(s)
    124         } else {
    125             None
    126         }
    127     }
    128 
    129     fn eat_number(&mut self) -> Option<f64> {
    130         if let Some(Token::Number(s)) = self.peek() {
    131             if let Ok(n) = s.parse::<f64>() {
    132                 self.pos += 1;
    133                 return Some(n);
    134             }
    135         }
    136         None
    137     }
    138 
    139     fn push_cell(&mut self, cell: Cell) -> CellId {
    140         let id = CellId(self.cells.len() as u32);
    141         self.cells.push(cell);
    142         id
    143     }
    144 
    145     // --- Attribute parsing ---
    146 
    147     fn try_parse_attribute(&mut self) -> Option<Attribute> {
    148         let cp = self.checkpoint();
    149 
    150         if !self.eat_open() {
    151             return None;
    152         }
    153 
    154         let sym = match self.eat_symbol() {
    155             Some(s) => s,
    156             None => {
    157                 self.restore(cp);
    158                 return None;
    159             }
    160         };
    161 
    162         let result = match sym {
    163             "shape" => self.eat_symbol().and_then(|s| {
    164                 let shape = match s {
    165                     "rectangle" => Shape::Rectangle,
    166                     "circle" => Shape::Circle,
    167                     "square" => Shape::Square,
    168                     _ => return None,
    169                 };
    170                 Some(Attribute::Shape(shape))
    171             }),
    172             "id" => self.eat_symbol().map(|s| Attribute::Id(s.to_string())),
    173             "name" => self.eat_string().map(|s| Attribute::Name(s.to_string())),
    174             "material" => self
    175                 .eat_string()
    176                 .map(|s| Attribute::Material(s.to_string())),
    177             "condition" => self
    178                 .eat_string()
    179                 .map(|s| Attribute::Condition(s.to_string())),
    180             "location" => self.eat_symbol().and_then(|s| {
    181                 let loc = match s {
    182                     "center" => Location::Center,
    183                     "floor" => Location::Floor,
    184                     "ceiling" => Location::Ceiling,
    185                     "top-of" => {
    186                         let id = self.eat_symbol()?;
    187                         Location::TopOf(id.to_string())
    188                     }
    189                     "near" => {
    190                         let id = self.eat_symbol()?;
    191                         Location::Near(id.to_string())
    192                     }
    193                     other => Location::Custom(other.to_string()),
    194                 };
    195                 Some(Attribute::Location(loc))
    196             }),
    197             "state" => self.eat_symbol().and_then(|s| {
    198                 let state = match s {
    199                     "on" => CellState::On,
    200                     "off" => CellState::Off,
    201                     "sleeping" => CellState::Sleeping,
    202                     _ => return None,
    203                 };
    204                 Some(Attribute::State(state))
    205             }),
    206             "type" => self.eat_symbol().map(|s| Attribute::Type(s.to_string())),
    207             "width" => self.eat_number().map(Attribute::Width),
    208             "height" => self.eat_number().map(Attribute::Height),
    209             "depth" => self.eat_number().map(Attribute::Depth),
    210             "position" => {
    211                 let x = self.eat_number();
    212                 let y = self.eat_number();
    213                 let z = self.eat_number();
    214                 match (x, y, z) {
    215                     (Some(x), Some(y), Some(z)) => Some(Attribute::Position(x, y, z)),
    216                     _ => None,
    217                 }
    218             }
    219             "rotation" => {
    220                 let x = self.eat_number();
    221                 let y = self.eat_number();
    222                 let z = self.eat_number();
    223                 match (x, y, z) {
    224                     (Some(x), Some(y), Some(z)) => Some(Attribute::Rotation(x, y, z)),
    225                     _ => None,
    226                 }
    227             }
    228             "model-url" => self
    229                 .eat_string()
    230                 .map(|s| Attribute::ModelUrl(s.to_string())),
    231             "tileset" => {
    232                 let mut names = Vec::new();
    233                 while let Some(s) = self.eat_string() {
    234                     names.push(s.to_string());
    235                 }
    236                 if names.is_empty() {
    237                     None
    238                 } else {
    239                     Some(Attribute::Tileset(names))
    240                 }
    241             }
    242             "data" => self.eat_string().map(|s| Attribute::Data(s.to_string())),
    243             _ => None,
    244         };
    245 
    246         match result {
    247             Some(attr) => {
    248                 if self.eat_close() {
    249                     Some(attr)
    250                 } else {
    251                     self.restore(cp);
    252                     None
    253                 }
    254             }
    255             None => {
    256                 self.restore(cp);
    257                 None
    258             }
    259         }
    260     }
    261 
    262     /// Parse zero or more attributes, returning the count.
    263     /// Attributes are pushed contiguously into self.attributes.
    264     fn parse_attributes(&mut self) -> u16 {
    265         let mut count = 0u16;
    266         while let Some(attr) = self.try_parse_attribute() {
    267             self.attributes.push(attr);
    268             count += 1;
    269         }
    270         count
    271     }
    272 
    273     // --- Cell parsing ---
    274 
    275     /// Parse attributes and an optional child cell (for room/space/object).
    276     fn parse_cell_attrs(&mut self, cell_type: CellType) -> Option<CellId> {
    277         let first_attr = self.attributes.len() as u32;
    278         let attr_count = self.parse_attributes();
    279 
    280         // Parse optional child cell — recursion may push to child_ids
    281         let opt_child = self.parse_cell();
    282 
    283         // Capture first_child AFTER recursion so nested children don't interleave
    284         let first_child = self.child_ids.len() as u32;
    285         let child_count;
    286         if let Some(child_id) = opt_child {
    287             self.child_ids.push(child_id);
    288             child_count = 1u16;
    289         } else {
    290             child_count = 0;
    291         }
    292 
    293         let id = self.push_cell(Cell {
    294             cell_type,
    295             first_attr,
    296             attr_count,
    297             first_child,
    298             child_count,
    299             parent: None,
    300         });
    301 
    302         // Set parent on children
    303         for i in 0..child_count {
    304             let child_id = self.child_ids[(first_child + i as u32) as usize];
    305             self.cells[child_id.0 as usize].parent = Some(id);
    306         }
    307 
    308         Some(id)
    309     }
    310 
    311     fn try_parse_named_cell(&mut self, name: &str, cell_type: CellType) -> Option<CellId> {
    312         let cp = self.checkpoint();
    313 
    314         if !self.eat_symbol_match(name) {
    315             self.restore(cp);
    316             return None;
    317         }
    318 
    319         match self.parse_cell_attrs(cell_type) {
    320             Some(id) => Some(id),
    321             None => {
    322                 self.restore(cp);
    323                 None
    324             }
    325         }
    326     }
    327 
    328     fn try_parse_room(&mut self) -> Option<CellId> {
    329         self.try_parse_named_cell("room", CellType::Room)
    330     }
    331 
    332     fn try_parse_space(&mut self) -> Option<CellId> {
    333         self.try_parse_named_cell("space", CellType::Space)
    334     }
    335 
    336     fn try_parse_group(&mut self) -> Option<CellId> {
    337         let cp = self.checkpoint();
    338 
    339         if !self.eat_symbol_match("group") {
    340             self.restore(cp);
    341             return None;
    342         }
    343 
    344         // Collect children — each parse_cell may recursively push to child_ids,
    345         // so we collect CellIds first and append ours after recursion completes
    346         let mut collected = Vec::new();
    347         while let Some(child_id) = self.parse_cell() {
    348             collected.push(child_id);
    349         }
    350 
    351         if collected.is_empty() {
    352             self.restore(cp);
    353             return None;
    354         }
    355 
    356         // Now append our children contiguously
    357         let first_child = self.child_ids.len() as u32;
    358         let child_count = collected.len() as u16;
    359         self.child_ids.extend_from_slice(&collected);
    360 
    361         let id = self.push_cell(Cell {
    362             cell_type: CellType::Group,
    363             first_attr: 0,
    364             attr_count: 0,
    365             first_child,
    366             child_count,
    367             parent: None,
    368         });
    369 
    370         // Set parent on children
    371         for i in 0..child_count {
    372             let child_id = self.child_ids[(first_child + i as u32) as usize];
    373             self.cells[child_id.0 as usize].parent = Some(id);
    374         }
    375 
    376         Some(id)
    377     }
    378 
    379     fn try_parse_tilemap(&mut self) -> Option<CellId> {
    380         self.try_parse_named_cell("tilemap", CellType::Tilemap)
    381     }
    382 
    383     fn try_parse_object(&mut self) -> Option<CellId> {
    384         let cp = self.checkpoint();
    385 
    386         let sym = self.eat_symbol()?;
    387 
    388         let obj_type = match sym {
    389             "table" => ObjectType::Table,
    390             "chair" => ObjectType::Chair,
    391             "door" => ObjectType::Door,
    392             "light" => ObjectType::Light,
    393             _ => ObjectType::Custom(sym.to_string()),
    394         };
    395 
    396         match self.parse_cell_attrs(CellType::Object(obj_type)) {
    397             Some(id) => Some(id),
    398             None => {
    399                 self.restore(cp);
    400                 None
    401             }
    402         }
    403     }
    404 
    405     fn parse_cell(&mut self) -> Option<CellId> {
    406         let cp = self.checkpoint();
    407 
    408         if !self.eat_open() {
    409             return None;
    410         }
    411 
    412         // Try each cell type
    413         let id = self
    414             .try_parse_group()
    415             .or_else(|| self.try_parse_room())
    416             .or_else(|| self.try_parse_space())
    417             .or_else(|| self.try_parse_tilemap())
    418             .or_else(|| self.try_parse_object());
    419 
    420         match id {
    421             Some(id) => {
    422                 if self.eat_close() {
    423                     Some(id)
    424                 } else {
    425                     self.restore(cp);
    426                     None
    427                 }
    428             }
    429             None => {
    430                 self.restore(cp);
    431                 None
    432             }
    433         }
    434     }
    435 }
    436 
    437 #[cfg(test)]
    438 mod tests {
    439     use super::*;
    440 
    441     #[test]
    442     fn test_parse_simple_room() {
    443         let space = parse("(room (name \"Test Room\") (width 10))").unwrap();
    444         assert_eq!(space.cells.len(), 1);
    445         let root = space.cell(space.root);
    446         assert_eq!(root.cell_type, CellType::Room);
    447         assert_eq!(root.attr_count, 2);
    448         assert_eq!(space.name(space.root), Some("Test Room"));
    449     }
    450 
    451     #[test]
    452     fn test_parse_object_with_child() {
    453         let input = "(table (name \"desk\") (light (name \"lamp\")))";
    454         let space = parse(input).unwrap();
    455         // light is cell 0, table is cell 1
    456         assert_eq!(space.cells.len(), 2);
    457         let root = space.cell(space.root);
    458         assert_eq!(root.cell_type, CellType::Object(ObjectType::Table));
    459         assert_eq!(root.child_count, 1);
    460 
    461         let children = space.children(space.root);
    462         let child = space.cell(children[0]);
    463         assert_eq!(child.cell_type, CellType::Object(ObjectType::Light));
    464         assert_eq!(space.name(children[0]), Some("lamp"));
    465     }
    466 
    467     #[test]
    468     fn test_parse_group() {
    469         let input = "(room (group (table (name \"t1\")) (chair (name \"c1\"))))";
    470         let space = parse(input).unwrap();
    471         // table=0, chair=1, group=2, room=3
    472         assert_eq!(space.cells.len(), 4);
    473         let root = space.cell(space.root);
    474         assert_eq!(root.cell_type, CellType::Room);
    475 
    476         // room has one child (group)
    477         let room_children = space.children(space.root);
    478         assert_eq!(room_children.len(), 1);
    479         let group = space.cell(room_children[0]);
    480         assert_eq!(group.cell_type, CellType::Group);
    481 
    482         // group has two children
    483         let group_children = space.children(room_children[0]);
    484         assert_eq!(group_children.len(), 2);
    485         assert_eq!(
    486             space.cell(group_children[0]).cell_type,
    487             CellType::Object(ObjectType::Table)
    488         );
    489         assert_eq!(
    490             space.cell(group_children[1]).cell_type,
    491             CellType::Object(ObjectType::Chair)
    492         );
    493     }
    494 
    495     #[test]
    496     fn test_parse_location_variants() {
    497         // Simple locations
    498         let space = parse("(table (location center))").unwrap();
    499         assert_eq!(space.location(space.root), Some(&Location::Center));
    500 
    501         let space = parse("(table (location floor))").unwrap();
    502         assert_eq!(space.location(space.root), Some(&Location::Floor));
    503 
    504         let space = parse("(table (location ceiling))").unwrap();
    505         assert_eq!(space.location(space.root), Some(&Location::Ceiling));
    506 
    507         // Relational locations
    508         let space = parse("(prop (location top-of obj1))").unwrap();
    509         assert_eq!(
    510             space.location(space.root),
    511             Some(&Location::TopOf("obj1".to_string()))
    512         );
    513 
    514         let space = parse("(chair (location near desk))").unwrap();
    515         assert_eq!(
    516             space.location(space.root),
    517             Some(&Location::Near("desk".to_string()))
    518         );
    519 
    520         // Custom/unknown location
    521         let space = parse("(light (location somewhere))").unwrap();
    522         assert_eq!(
    523             space.location(space.root),
    524             Some(&Location::Custom("somewhere".to_string()))
    525         );
    526     }
    527 
    528     #[test]
    529     fn test_parse_tilemap() {
    530         let input = r#"(tilemap (width 10) (height 10) (tileset "grass" "stone") (data "0"))"#;
    531         let space = parse(input).unwrap();
    532         let root = space.cell(space.root);
    533         assert_eq!(root.cell_type, CellType::Tilemap);
    534         assert_eq!(space.width(space.root), Some(10.0));
    535         assert_eq!(space.height(space.root), Some(10.0));
    536         assert_eq!(
    537             space.tileset(space.root),
    538             Some(&vec!["grass".to_string(), "stone".to_string()])
    539         );
    540         assert_eq!(space.data(space.root), Some("0"));
    541     }
    542 
    543     #[test]
    544     fn test_parse_tilemap_in_group() {
    545         let input = r#"(space (name "Test") (group (tilemap (width 5) (height 5) (tileset "grass") (data "0")) (table (id t1))))"#;
    546         let space = parse(input).unwrap();
    547         let group_id = space.children(space.root)[0];
    548         let children = space.children(group_id);
    549         assert_eq!(children.len(), 2);
    550         assert_eq!(space.cell(children[0]).cell_type, CellType::Tilemap);
    551         assert_eq!(
    552             space.cell(children[1]).cell_type,
    553             CellType::Object(ObjectType::Table)
    554         );
    555     }
    556 
    557     #[test]
    558     fn test_location_roundtrip() {
    559         use crate::serializer::serialize;
    560 
    561         let input = r#"(room (group (table (id obj1) (position 0 0 0)) (prop (id obj2) (location top-of obj1))))"#;
    562         let space1 = parse(input).unwrap();
    563         let serialized = serialize(&space1);
    564         let space2 = parse(&serialized).unwrap();
    565 
    566         // Find obj2 in both
    567         let group1 = space1.children(space1.root)[0];
    568         let obj2_1 = space1.children(group1)[1];
    569         assert_eq!(
    570             space1.location(obj2_1),
    571             Some(&Location::TopOf("obj1".to_string()))
    572         );
    573 
    574         let group2 = space2.children(space2.root)[0];
    575         let obj2_2 = space2.children(group2)[1];
    576         assert_eq!(
    577             space2.location(obj2_2),
    578             Some(&Location::TopOf("obj1".to_string()))
    579         );
    580     }
    581 }