vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
//! Where a name in an inline constraint resolves (constraints plan C19; LRM
//! 1800-2023 18.7 and 18.7.1).
//!
//! `obj.randomize() with { ... }` reads names from two scopes: the class of
//! `obj` (the *object* scope) and the scope containing the call (the *call*
//! scope: locals, subroutine arguments, the calling class's own properties,
//! module variables). slang performs this resolution when it binds the call,
//! so the expression tree already names the right symbol; what it does not say
//! is *which scope* that symbol was found in, and the difference matters to the
//! solver: an object name is a random variable or a property of the object
//! being solved, a call name is a constant of the solve whose value the call
//! reads when it is made. [`resolve`] is the rule slang applies, restated from
//! the LRM so the compiler can classify each name occurrence, and so the
//! 18.7/18.7.1 examples are pinned by tests that do not need a design.
//!
//! The rule, in the LRM's order:
//!
//! 1. a name qualified with `local::` resolves in the call scope, whether or
//!    not the object has a member of that name, and whether or not a
//!    restricted block lists it (18.7.1);
//! 2. otherwise, in a *restricted* block (`with (x, y) { ... }`), only the
//!    listed names may resolve into the object; every other name resolves in
//!    the call scope (18.7);
//! 3. otherwise the object scope is searched first, then the call scope
//!    (an unrestricted block, 18.7).

/// The scope a name in an inline constraint resolves in.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scope {
    /// The class of the randomized object.
    Object,
    /// The scope containing the `randomize()` call.
    Call,
}

/// The scope `name` (the first identifier of the written name, after any
/// `local::`) resolves in.
///
/// * `local_qualified`: the name was written `local::name`;
/// * `restriction`: the identifier list of a restricted block, `None` for an
///   unrestricted one. An *empty* list (`with () {...}`) is unrestricted too:
///   the grammar makes the list optional inside the parentheses and says
///   nothing about the empty case, and the front end (slang) treats it as
///   unrestricted, which is the resolution the expression tree already
///   reflects;
/// * `object_declares`: whether the randomized object's class has a member
///   called `name` (a property, for the purpose of a read).
pub fn resolve<S: AsRef<str>>(
    name: &str,
    local_qualified: bool,
    restriction: Option<&[S]>,
    object_declares: bool,
) -> Scope {
    if local_qualified {
        return Scope::Call;
    }
    if let Some(list) = restriction
        && !list.is_empty()
        && !list.iter().any(|n| n.as_ref() == name)
    {
        return Scope::Call;
    }
    if object_declares {
        Scope::Object
    } else {
        Scope::Call
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const NONE: Option<&[&str]> = None;

    // 18.7: `f.randomize() with {x < y + z;}` in `C2::doit(C1 f, integer x,
    // integer z)`, with `C1 { rand integer x; }` and `C2 { integer x, y; }`.
    // "x is a member of class C1 and hides the x in class C2. It also hides the
    // x argument in the doit() task. y is a member of C2. z is a local
    // argument."
    #[test]
    fn unrestricted_block_searches_the_object_first() {
        // object class C1 declares x only
        assert_eq!(resolve("x", false, NONE, true), Scope::Object);
        assert_eq!(resolve("y", false, NONE, false), Scope::Call);
        assert_eq!(resolve("z", false, NONE, false), Scope::Call);
    }

    // 18.7: `obj.randomize() with (x) { x < y; }` in `F(C obj, integer y)`:
    // "only x is resolved into the object obj since only x is listed ... The
    // reference to y will never bind into obj even if a later change adds a
    // property named y into class C."
    #[test]
    fn restricted_block_lets_only_listed_names_into_the_object() {
        let list = Some(&["x"][..]);
        assert_eq!(resolve("x", false, list, true), Scope::Object);
        // y is not listed: call scope even when the object declares a `y`
        assert_eq!(resolve("y", false, list, false), Scope::Call);
        assert_eq!(resolve("y", false, list, true), Scope::Call);
    }

    // a listed name the object does not declare falls through to the call
    // scope like any unresolved name
    #[test]
    fn a_listed_name_the_object_lacks_resolves_in_the_call_scope() {
        let list = Some(&["x"][..]);
        assert_eq!(resolve("x", false, list, false), Scope::Call);
    }

    // `with () {...}` is unrestricted, as the front end binds it
    #[test]
    fn empty_restriction_list_is_unrestricted() {
        let list = Some(&[] as &[&str]);
        assert_eq!(resolve("x", false, list, true), Scope::Object);
        assert_eq!(resolve("y", false, list, false), Scope::Call);
    }

    // 18.7.1: `obj.randomize() with { x < local::x; }` in `F(C obj, integer
    // x)`: "the unqualified name, x, binds to the property of class C ... while
    // the qualified name local::x binds to the argument of the function F()".
    #[test]
    fn local_qualifier_bypasses_the_object() {
        assert_eq!(resolve("x", false, NONE, true), Scope::Object);
        assert_eq!(resolve("x", true, NONE, true), Scope::Call);
    }

    // 18.7: "When the parenthesized identifier_list is present and the local::
    // qualifier is used, the qualified name shall resolve starting in the scope
    // containing the randomize() method call independent of whether the name is
    // present in the identifier_list."
    #[test]
    fn local_qualifier_wins_over_a_restriction_list() {
        let list = Some(&["x"][..]);
        assert_eq!(resolve("x", true, list, true), Scope::Call);
        assert_eq!(resolve("x", false, list, true), Scope::Object);
    }
}