Q:

Is possible in a single pass build a FULL JOIN for a in-memory structure (not using sql!)

I'm building a in-memory columnar relational engine. For extract the values I want to do late materialization where I collect the positions/indices where a match was found and at the end collect the values.
Now implementing joins I fail to see how do a general join algorithm that cover all the other cases. Left, Right & Inner are easy, but FULL/Outer not. This is my naive implementation with nested-loops:
pub fn join(&self, compare:&BoolExpr) -> JoinPos
{
    //Allocate at least for inner joins...
    let total = cmp::max(self.left.len(), self.right.len());

    let mut cols_left  = Vec::with_capacity(total);
    let mut cols_right = Vec::with_capacity(total);
    let mut found = false;

    while !self.left.eof() {
        let left = self.left.tuple();
        while !self.right.eof() {
            let right = self.right.tuple();
            if compare(&left, &right) {
                //A match found. Record positions of both cursors
                cols_left.push(self.left.pos() as isize);
                cols_right.push(self.right.pos() as isize);
                found = true;
            }
            self.right.next();
        }
        //Not found a single match at the right cursor..
        if !found {
            cols_left.push(self.left.pos() as isize);
            cols_right.push(-1);
        }
        found = false;
        self.left.next();
        self.right.first();
    }

    JoinPos {
        left:cols_left,
        right:cols_right,
    }
}

Now, the problem is that I can find when the left was not found in the right but not the opposite without another pass:
Input:
L= [1, 2, 3]
R= [2, 3, 4]

Result. It capture the positions that match. -1 if not found
L | R
======
1   -1
2   1
3   2
-1  3

Thanks to @philipxy I have a working solution:
pub fn join(&self, compare:&BoolExpr) -> JoinPos
{
    let total = cmp::max(self.left.len(), self.right.len());

    let mut right_not_founds = HashSet::new();
    let mut cols_left  = Vec::with_capacity(total);
    let mut cols_right = Vec::with_capacity(total);
    let mut found = false;
    let mut first = true;

    while !self.left.eof() {
        let left = self.left.tuple(&self.cols_left);
        while !self.right.eof() {
            let right = self.right.tuple(&self.cols_right);
            if first {
                right_not_founds.insert(self.right.pos());
            }

            if compare(&left, &right) {
                cols_left.push(self.left.pos() as isize);
                cols_right.push(self.right.pos() as isize);
                right_not_founds.remove(&self.right.pos());

                found = true;
            }
            self.right.next();
        }
        if !found {
            cols_left.push(self.left.pos() as isize);
            cols_right.push(-1);
        }
        found = false;
        first = false;
        self.left.next();
        self.right.first();
    }

    for pos in right_not_founds {
        cols_left.push(-1);
        cols_right.push(pos as isize);
    }

    JoinPos {
        left:cols_left,
        right:cols_right,
    }
}

A:

Full join returns inner join tuples union unmatched left table tuples extended by nulls union unmatched right table tuples extended by nulls.
Currently your code outputs left join tuples. Each iteration of the outer loop outputs more tuples that are in the inner join or it outputs the null-extension of a left table tuple that did not match any right table tuple. To output full join tuples you must also output the null-extension of each right table tuple that did not match any left table tuple. You can do this as follows: Define a set variable before the loops. It will eventually contain all positions/indices for right table tuples that did not match any left table tuple. Immediately before the if compare, if it is the first iteration of the outer loop then insert the right tuple position/index into the set. Inside the if  compare's nested block remove the right tuple position/index from the set.

