Changeset - 699bec97a862
[Not reviewed]
0 2 0
mh - 4 years ago 2021-12-21 15:39:08
contact@maxhenger.nl
Fix longstanding variable scoping bug, add select statement test
2 files changed with 32 insertions and 14 deletions:
0 comments (0 inline, 0 general)
src/protocol/parser/pass_validation_linking.rs
Show inline comments
 
@@ -1520,248 +1520,251 @@ impl PassValidationLinking {
 
        let mut scope_idx = 0;
 
        while var_idx < var_section.len() || scope_idx < scope_section.len() {
 
            let relative_var_pos = if var_idx < var_section.len() {
 
                ctx.heap[var_section[var_idx]].relative_pos_in_block
 
            } else {
 
                i32::MAX
 
            };
 

	
 
            let relative_scope_pos = if scope_idx < scope_section.len() {
 
                ctx.heap[scope_section[scope_idx]].as_block().scope_node.relative_pos_in_parent
 
            } else {
 
                i32::MAX
 
            };
 

	
 
            debug_assert!(!(relative_var_pos == i32::MAX && relative_scope_pos == i32::MAX));
 

	
 
            // In certain cases the relative variable position is the same as
 
            // the scope position (insertion of binding variables). In that case
 
            // the variable should be treated first
 
            if relative_var_pos <= relative_scope_pos {
 
                let var = &mut ctx.heap[var_section[var_idx]];
 
                var.unique_id_in_scope = var_counter;
 
                var_counter += 1;
 
                var_idx += 1;
 
            } else {
 
                // Boy oh boy
 
                let block_id = ctx.heap[scope_section[scope_idx]].as_block().this;
 
                self.visit_block_and_assign_local_ids(ctx, block_id, var_counter);
 
                scope_idx += 1;
 
            }
 
        }
 

	
 
        var_section.forget();
 
        scope_section.forget();
 

	
 
        // Done assigning all IDs, assign the last ID to the block statement scope
 
        let block_stmt = &mut ctx.heap[block_id];
 
        block_stmt.next_unique_id_in_scope = var_counter;
 
    }
 

	
 
    fn resolve_pending_control_flow_targets(&mut self, ctx: &mut Ctx) -> Result<(), ParseError> {
 
        for entry in &self.control_flow_stmts {
 
            let stmt = &ctx.heap[entry.statement];
 

	
 
            match stmt {
 
                Statement::Break(stmt) => {
 
                    let stmt_id = stmt.this;
 
                    let target_while_id = Self::resolve_break_or_continue_target(ctx, entry, stmt.span, &stmt.label)?;
 
                    let target_while_stmt = &ctx.heap[target_while_id];
 
                    let target_end_while_id = target_while_stmt.end_while;
 
                    debug_assert!(!target_end_while_id.is_invalid());
 

	
 
                    let break_stmt = &mut ctx.heap[stmt_id];
 
                    break_stmt.target = target_end_while_id;
 
                },
 
                Statement::Continue(stmt) => {
 
                    let stmt_id = stmt.this;
 
                    let target_while_id = Self::resolve_break_or_continue_target(ctx, entry, stmt.span, &stmt.label)?;
 

	
 
                    let continue_stmt = &mut ctx.heap[stmt_id];
 
                    continue_stmt.target = target_while_id;
 
                },
 
                Statement::Goto(stmt) => {
 
                    let stmt_id = stmt.this;
 
                    let target_id = Self::find_label(entry.in_scope, ctx, &stmt.label)?;
 
                    let target_stmt = &ctx.heap[target_id];
 
                    if entry.in_sync != target_stmt.in_sync {
 
                        // Nested sync not allowed. And goto can only go to
 
                        // outer scopes, so we must be escaping from a sync.
 
                        debug_assert!(target_stmt.in_sync.is_invalid());    // target not in sync
 
                        debug_assert!(!entry.in_sync.is_invalid()); // but the goto is in sync
 
                        let goto_stmt = &ctx.heap[stmt_id];
 
                        let sync_stmt = &ctx.heap[entry.in_sync];
 
                        return Err(
 
                            ParseError::new_error_str_at_span(&ctx.module().source, goto_stmt.span, "goto may not escape the surrounding synchronous block")
 
                            .with_info_str_at_span(&ctx.module().source, target_stmt.label.span, "this is the target of the goto statement")
 
                            .with_info_str_at_span(&ctx.module().source, sync_stmt.span, "which will jump past this statement")
 
                        );
 
                    }
 

	
 
                    let goto_stmt = &mut ctx.heap[stmt_id];
 
                    goto_stmt.target = target_id;
 
                },
 
                _ => unreachable!("cannot resolve control flow target for {:?}", stmt),
 
            }
 
        }
 

	
 
        return Ok(())
 
    }
 

	
 
    //--------------------------------------------------------------------------
 
    // Utilities
 
    //--------------------------------------------------------------------------
 

	
 
    /// Adds a local variable to the current scope. It will also annotate the
 
    /// `Local` in the AST with its relative position in the block.
 
    fn checked_add_local(&mut self, ctx: &mut Ctx, target_scope: Scope, relative_pos: i32, id: VariableId) -> Result<(), ParseError> {
 
    fn checked_add_local(&mut self, ctx: &mut Ctx, target_scope: Scope, target_relative_pos: i32, id: VariableId) -> Result<(), ParseError> {
 
        debug_assert!(target_scope.is_block());
 
        let local = &ctx.heap[id];
 
        println!("DEBUG: Adding local '{}' at relative_pos {} in scope {:?}", local.identifier.value.as_str(), relative_pos, target_scope);
 

	
 
        let mut scope = target_scope;
 
        println!("DEBUG: Adding local '{}' at relative_pos {} in scope {:?}", local.identifier.value.as_str(), target_relative_pos, target_scope);
 

	
 
        // We immediately go to the parent scope. We check the target scope
 
        // in the call at the end. That is also where we check for collisions
 
        // with symbols.
 
        let block = &ctx.heap[target_scope.to_block()];
 
        let mut scope = block.scope_node.parent;
 
        let mut cur_relative_pos = block.scope_node.relative_pos_in_parent;
 
        loop {
 
            // We immediately go to the parent scope. We check the current scope
 
            // in the call at the end. Likewise for checking the symbol table.
 
            let block = &ctx.heap[scope.to_block()];
 

	
 
            scope = block.scope_node.parent;
 
            if let Scope::Definition(definition_id) = scope {
 
                // At outer scope, check parameters of function/component
 
                for parameter_id in ctx.heap[definition_id].parameters() {
 
                    let parameter = &ctx.heap[*parameter_id];
 
                    if local.identifier == parameter.identifier {
 
                        return Err(
 
                            ParseError::new_error_str_at_span(
 
                                &ctx.module().source, local.identifier.span, "Local variable name conflicts with parameter"
 
                            ).with_info_str_at_span(
 
                                &ctx.module().source, parameter.identifier.span, "Parameter definition is found here"
 
                            )
 
                        );
 
                    }
 
                }
 

	
 
                // No collisions
 
                break;
 
            }
 

	
 
            // If here then the parent scope is a block scope
 
            let local_relative_pos = ctx.heap[scope.to_block()].scope_node.relative_pos_in_parent;
 
            let block = &ctx.heap[scope.to_block()];
 

	
 
            for other_local_id in &block.locals {
 
                let other_local = &ctx.heap[*other_local_id];
 
                // Position check in case another variable with the same name
 
                // is defined in a higher-level scope, but later than the scope
 
                // in which the current variable resides.
 
                if local.this != *other_local_id &&
 
                    local_relative_pos >= other_local.relative_pos_in_block &&
 
                    cur_relative_pos >= other_local.relative_pos_in_block &&
 
                    local.identifier == other_local.identifier {
 
                    // Collision within this scope
 
                    return Err(
 
                        ParseError::new_error_str_at_span(
 
                            &ctx.module().source, local.identifier.span, "Local variable name conflicts with another variable"
 
                        ).with_info_str_at_span(
 
                            &ctx.module().source, other_local.identifier.span, "Previous variable is found here"
 
                        )
 
                    );
 
                }
 
            }
 

	
 
            scope = block.scope_node.parent;
 
            cur_relative_pos = block.scope_node.relative_pos_in_parent;
 
        }
 

	
 
        // No collisions in any of the parent scope, attempt to add to scope
 
        self.checked_at_single_scope_add_local(ctx, target_scope, relative_pos, id)
 
        self.checked_at_single_scope_add_local(ctx, target_scope, target_relative_pos, id)
 
    }
 

	
 
    /// Adds a local variable to the specified scope. Will check the specified
 
    /// scope for variable conflicts and the symbol table for global conflicts.
 
    /// Will NOT check parent scopes of the specified scope.
 
    fn checked_at_single_scope_add_local(
 
        &mut self, ctx: &mut Ctx, scope: Scope, relative_pos: i32, id: VariableId
 
    ) -> Result<(), ParseError> {
 
        // Check the symbol table for conflicts
 
        {
 
            let cur_scope = SymbolScope::Definition(self.def_type.definition_id());
 
            let ident = &ctx.heap[id].identifier;
 
            if let Some(symbol) = ctx.symbols.get_symbol_by_name(cur_scope, &ident.value.as_bytes()) {
 
                return Err(ParseError::new_error_str_at_span(
 
                    &ctx.module().source, ident.span,
 
                    "local variable declaration conflicts with symbol"
 
                ).with_info_str_at_span(
 
                    &ctx.module().source, symbol.variant.span_of_introduction(&ctx.heap), "the conflicting symbol is introduced here"
 
                ));
 
            }
 
        }
 

	
 
        // Check the specified scope for conflicts
 
        let local = &ctx.heap[id];
 

	
 
        debug_assert!(scope.is_block());
 
        let block = &ctx.heap[scope.to_block()];
 
        for other_local_id in &block.locals {
 
            let other_local = &ctx.heap[*other_local_id];
 
            if local.this != other_local.this &&
 
                // relative_pos >= other_local.relative_pos_in_block &&
 
                local.identifier == other_local.identifier {
 
                // Collision
 
                return Err(
 
                    ParseError::new_error_str_at_span(
 
                        &ctx.module().source, local.identifier.span, "Local variable name conflicts with another variable"
 
                    ).with_info_str_at_span(
 
                        &ctx.module().source, other_local.identifier.span, "Previous variable is found here"
 
                    )
 
                );
 
            }
 
        }
 

	
 
        // No collisions
 
        let block = &mut ctx.heap[scope.to_block()];
 
        block.locals.push(id);
 

	
 
        let local = &mut ctx.heap[id];
 
        local.relative_pos_in_block = relative_pos;
 

	
 
        Ok(())
 
    }
 

	
 
    /// Finds a variable in the visitor's scope that must appear before the
 
    /// specified relative position within that block.
 
    fn find_variable(&self, ctx: &Ctx, mut relative_pos: i32, identifier: &Identifier) -> Option<VariableId> {
 
        println!("DEBUG: Calling find_variable for '{}' at relative_pos {}", identifier.value.as_str(), relative_pos);
 
        debug_assert!(self.cur_scope.is_block());
 

	
 
        // No need to use iterator over namespaces if here
 
        let mut scope = &self.cur_scope;
 
        
 
        loop {
 
            debug_assert!(scope.is_block());
 
            let block = &ctx.heap[scope.to_block()];
 
            println!("DEBUG: > Looking in block {:?} at relative_pos {}", scope.to_block().0, relative_pos);
 
            
 
            for local_id in &block.locals {
 
                let local = &ctx.heap[*local_id];
 
                
 
                if local.relative_pos_in_block < relative_pos && identifier == &local.identifier {
 
                    println!("DEBUG: > Matched at local with relative_pos {}", local.relative_pos_in_block);
 
                    return Some(*local_id);
 
                }
 
            }
 

	
 
            scope = &block.scope_node.parent;
 
            if !scope.is_block() {
 
                // Definition scope, need to check arguments to definition
 
                match scope {
 
                    Scope::Definition(definition_id) => {
 
                        let definition = &ctx.heap[*definition_id];
 
                        for parameter_id in definition.parameters() {
 
                            let parameter = &ctx.heap[*parameter_id];
 
                            if identifier == &parameter.identifier {
 
                                return Some(*parameter_id);
 
                            }
 
                        }
 
                    },
 
                    _ => unreachable!(),
 
                }
 

	
 
                // Variable could not be found
 
                return None
 
            } else {
 
                relative_pos = block.scope_node.relative_pos_in_parent;
src/protocol/tests/parser_validation.rs
Show inline comments
 
@@ -558,200 +558,215 @@ fn test_incorrect_modifying_operators() {
 

	
 
    Tester::new_single_source_expect_err(
 
        "inside function",
 
        "func f(u32 a) -> u32 { auto b = 0; auto c = f(a += 2); }"
 
    ).error(|e| { e.assert_msg_has(0, "assignments are statements"); });
 

	
 
    Tester::new_single_source_expect_err(
 
        "inside tuple",
 
        "func f(u32 a) -> u32 { auto b = (a += 2, a /= 2); return 0; }"
 
    ).error(|e| { e.assert_msg_has(0, "assignments are statements"); });
 
}
 

	
 
#[test]
 
fn test_variable_introduction_in_scope() {
 
    Tester::new_single_source_expect_err(
 
        "variable use before declaration",
 
        "func f() -> u8 { return thing; auto thing = 5; }"
 
    ).error(|e| { e.assert_msg_has(0, "unresolved variable"); });
 

	
 
    Tester::new_single_source_expect_err(
 
        "variable use in declaration",
 
        "func f() -> u8 { auto thing = 5 + thing; return thing; }"
 
    ).error(|e| { e.assert_msg_has(0, "unresolved variable"); });
 

	
 
    Tester::new_single_source_expect_ok(
 
        "variable use after declaration",
 
        "func f() -> u8 { auto thing = 5; return thing; }"
 
    );
 

	
 
    Tester::new_single_source_expect_err(
 
        "variable use of closed scope",
 
        "func f() -> u8 { { auto thing = 5; } return thing; }"
 
    ).error(|e| { e.assert_msg_has(0, "unresolved variable"); });
 
}
 

	
 
#[test]
 
fn test_correct_select_statement() {
 

	
 
    Tester::new_single_source_expect_ok(
 
        "guard variable decl",
 
        "
 
        primitive f() {
 
            channel<u32> unused -> input;
 

	
 
            u32 outer_value = 0;
 
            sync select {
 
                auto in_same_guard = get(input) -> {} // decl A1
 
                auto in_same_gaurd = get(input) -> {} // decl A2
 
                auto in_guard_and_block = get(input) -> {} // decl B1
 
                outer_value = get(input) -> { auto in_guard_and_block = outer_value; } // decl B2
 
            }
 
        }
 
        "
 
    );
 

	
 
    Tester::new_single_source_expect_ok(
 
        "empty select",
 
        "primitive f() { sync select {} }"
 
    );
 

	
 
    Tester::new_single_source_expect_ok(
 
        "mixed uses", "
 
        primitive f() {
 
            channel unused_output -> input;
 
            u32 outer_value = 0;
 
            sync select {
 
                outer_value = get(input) -> outer_value = 0;
 
                auto new_value = get(input) -> {
 
                    outer_value = new_value;
 
                }
 
                get(input) + get(input) ->
 
                    outer_value = 8;
 
                get(input) ->
 
                    {}
 
                outer_value %= get(input) -> {
 
                    outer_value *= outer_value;
 
                    auto new_value = get(input);
 
                    outer_value += new_value;
 
                }
 
            }
 
        }
 
        "
 
    );
 
}
 

	
 
#[test]
 
fn test_incorrect_select_statement() {
 
    Tester::new_single_source_expect_err(
 
        "outside sync",
 
        "primitive f() { select {} }"
 
    ).error(|e| { e
 
        .assert_num(1)
 
        .assert_occurs_at(0, "select")
 
        .assert_msg_has(0, "inside sync blocks");
 
    });
 

	
 
    Tester::new_single_source_expect_ok(
 
    Tester::new_single_source_expect_err(
 
        "variable in previous block",
 
        "primitive f() {
 
            channel<u32> tx -> rx;
 
            u32 a = 0; // this one will be shadowed
 
            sync select { auto a = get(rx) -> {} }
 
        }"
 
    );
 
    ).error(|e| { e
 
        .assert_num(2)
 
        .assert_occurs_at(0, "a = get").assert_msg_has(0, "variable name conflicts")
 
        .assert_occurs_at(1, "a = 0").assert_msg_has(1, "Previous variable");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "put inside arm",
 
        "primitive f() {
 
            channel<u32> a -> b;
 
            sync select { put(a) -> {} }
 
        }"
 
    ).error(|e| { e
 
        .assert_occurs_at(0, "put")
 
        .assert_msg_has(0, "may not occur");
 
    });
 
}
 

	
 
#[test]
 
fn test_incorrect_goto_statement() {
 
    Tester::new_single_source_expect_err(
 
        "goto missing var in same scope",
 
        "func f() -> u32 {
 
            goto exit;
 
            auto v = 5;
 
            exit: return 0;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(3)
 
        .assert_occurs_at(0, "exit;").assert_msg_has(0, "skips over a variable")
 
        .assert_occurs_at(1, "exit:").assert_msg_has(1, "jumps to this label")
 
        .assert_occurs_at(2, "v = 5").assert_msg_has(2, "skips over this variable");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "goto missing var in outer scope",
 
        "func f() -> u32 {
 
            if (true) {
 
                goto exit;
 
            }
 
            auto v = 0;
 
            exit: return 1;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(3)
 
        .assert_occurs_at(0, "exit;").assert_msg_has(0, "skips over a variable")
 
        .assert_occurs_at(1, "exit:").assert_msg_has(1, "jumps to this label")
 
        .assert_occurs_at(2, "v = 0").assert_msg_has(2, "skips over this variable");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "goto jumping into scope",
 
        "func f() -> u32 {
 
            goto nested;
 
            {
 
                nested: return 0;
 
            }
 
            return 1;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(1)
 
        .assert_occurs_at(0, "nested;")
 
        .assert_msg_has(0, "could not find this label");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "goto jumping outside sync",
 
        "primitive f() {
 
            sync { goto exit; }
 
            exit: u32 v = 0;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(3)
 
        .assert_occurs_at(0, "goto exit;").assert_msg_has(0, "not escape the surrounding sync")
 
        .assert_occurs_at(1, "exit: u32 v").assert_msg_has(1, "target of the goto")
 
        .assert_occurs_at(2, "sync {").assert_msg_has(2, "jump past this");
 
    })
 
}
 

	
 
#[test]
 
fn test_incorrect_while_statement() {
 
    // Just testing the error cases caught at compile-time. Other ones need
 
    // evaluation testing
 
    Tester::new_single_source_expect_err(
 
        "break wrong earlier loop",
 
        "func f() -> u32 {
 
            target: while (true) {}
 
            while (true) { break target; }
 
            return 0;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(2)
 
        .assert_occurs_at(0, "target; }").assert_msg_has(0, "not nested under the target")
 
        .assert_occurs_at(1, "target: while").assert_msg_has(1, "is found here");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "break wrong later loop",
 
        "func f() -> u32 {
 
            while (true) { break target; }
 
            target: while (true) {}
 
            return 0;
 
        }"
 
    ).error(|e| { e
 
        .assert_num(2)
 
        .assert_occurs_at(0, "target; }").assert_msg_has(0, "not nested under the target")
 
        .assert_occurs_at(1, "target: while").assert_msg_has(1, "is found here");
 
    });
 

	
 
    Tester::new_single_source_expect_err(
 
        "break outside of sync",
 
        "primitive f() {
0 comments (0 inline, 0 general)