1
votes

I've been working on creating a Table struct that can resolve to a Val trait object along with other data types (Number implemented as an example) and can be seen in the Rust Playground here. I haven't been able to get past this borrow/lifetime issue but the idea is that Number and Table are both Val objects. However, any attempt to write the object to a String buffer causes a failure in compilation.

What am I doing wrong here? And what is the rationale for the borrow checker to operate in this way?

use std::fmt;
use std::fmt::Write;

struct Number(f64);

struct Table<'a> {
    rows: Vec<Row<'a>>
    // Will have column vector as a member
}

impl <'a>Table<'a> {
    fn new(rows: Vec<Vec<Box<dyn Val>>>) -> Table {
        let mut table = Table {
            rows: rows.into_iter().map(|r| {
                Row {
                    parent: std::ptr::null_mut(),
                    cells: r
                }
            }).collect()
        };

        let parent = &mut table as *mut Table;

        table.rows = table.rows.into_iter().map(|mut r| {
            r.parent = parent;
            r
        }).collect();

        table
    }
}

struct Row<'a> {
    parent: *mut Table<'a>,
    cells: Vec<Box<dyn Val<'a>>>
}

impl <'a>Row<'a> {
    fn to_str(&'a self, buf: &mut String) -> fmt::Result {
        let mut cell_iter = self.cells.iter().enumerate().peekable();
        let _parent = unsafe { self.parent.as_ref() }; // Real implementation will need parent ref
        while let Some((idx,_c)) = cell_iter.next() { // Real implementation will cycle through columns in parent struct
            match self.cells.get(idx) {
                Some(v) => v.to_str(buf),
                None => Ok(())
            }?;
            if let Some(_) = cell_iter.peek() {
                write!(buf, ",")?;
            }
        }
        Ok(())
    }
}

pub trait Val<'a> {
    fn to_str(&'a self, buf: &mut String) -> fmt::Result;
}

pub trait ObjWriter<'a> {
    fn to_str(&'a self, buf: &'a mut String) -> fmt::Result;
}

impl <'a>ObjWriter<'a> for dyn Val<'a> {
    fn to_str(&'a self, buf: &mut String) -> fmt::Result { self.to_str(buf) }
}

impl <'a>Val<'a> for Table<'a> {
    fn to_str(&'a self, buf: &mut String) -> fmt::Result {
        write!(buf, "(START TABLE:")?;
        let mut row_iter = self.rows.iter().peekable();
        while let Some(r) = row_iter.next() {
            r.to_str(buf)?;
            write!(buf, "\n")?;
        }
        write!(buf, "END TABLE)")
    }
}

impl Number {
    fn to_str(&self, buf: &mut String) -> fmt::Result {
        write!(buf,"{}",self.0)
    }
}

impl <'a>Val<'a> for Number {
    fn to_str(&self, buf: &mut String) -> fmt::Result {
        self.to_str(buf)
    }
}

fn main() {
    let table = Table::new(vec![
        vec![Box::new(Number(0.5)),Box::new(Table::new(Vec::new()))],
        vec![Box::new(Table::new(Vec::new())),Box::new(Number(0.5))],
    ]);

    let mut buf = String::new();
    table.to_str(&mut buf);
    println!("{}",buf)
}
error[E0597]: `table` does not live long enough
   --> src/main.rs:98:5
    |
92  |       let table = Table::new(vec![
    |  ____________________________-
93  | |         vec![Box::new(Number(0.5)),Box::new(Table::new(Vec::new()))],
94  | |         vec![Box::new(Table::new(Vec::new())),Box::new(Number(0.5))],
95  | |     ]);
    | |_____- cast requires that `table` is borrowed for `'static`
...
98  |       table.to_str(&mut buf);
    |       ^^^^^ borrowed value does not live long enough
99  |       println!("{}",buf)
100 |   }
    |   - `table` dropped here while still borrowed
1
I think the 'static in the error message comes from Box<dyn Foo> being a shorthand for Box<dyn Foo + 'static>. I tried adding + 'a to your dyn boxes, but I got a different error related to lifetimes. Not sure this helps, but maybe it nudges you in the right direction.user4815162342
Thanks for playing :) I've tried many different variations of lifetime declarations and am still unsuccessful! I've actually got the exact same error in another version of this but neither works. Thanks anyway!candronikos
Your parent pointers are invalidated as soon as new returns.trentcl
You've got way too many 'as here, it's very confusing. You almost never want &'a self in a function parameter, so those would be the first to get rid of. The problem is it's tricky to remove lifetimes in code that uses unsafe especially when the code is already known to be unsound, as I already pointed out this code is. How are you going to deal with the problem that as soon as table moves, all the pointers to it are immediately invalidated? Once you have that figured out, we can talk about how to prove it correct with lifetimes.trentcl
This version, for example, compiles, but is wrong: click for undefined behaviortrentcl

1 Answers

0
votes

I did as suggested trentcl, noting that my pointers would be invalidated on move, I refactored my code so that any Row methods would take the parent table's reference as the first parameter. Then I removed all the <'a> followed the compiler errors until it worked.

So the whole problem was allowing the lifetimes to get out of control and using unsafe methods to get parent references. Go figure...