Due: before class on Wednesday 05 November

These problems are from the second half of Unit 3 and from class 4.1.

  1. Consider the following code in Rust:

    fn eat_num(val: i32) { /* ... */ }
    fn eat_str(val: String) { /* ... */ }
    
    fn main() {
        let my_num = 100000;
        eat_num(my_num);
        println!("{}", my_num); // Line A
    
        let my_str = String::from("snickers");
        eat_str(my_str);
        println!("{}", my_str); // Line B
    }

    Why does Line A compile, but Line B causes a “use of moved value” error? (Select all that apply)

    1. i32 is a “Plain Old Data” (POD) type that lives entirely on the stack.
    2. i32 implements the Copy trait, so my_num is copied into eat_num instead of being moved.
    3. String manages heap-allocated data and does not implement the Copy trait, so ownership of my_str is moved into eat_str.
    4. Passing my_str as &my_str (a borrow) would fix the error at Line B by passing an immutable reference instead of moving ownership.
    5. String values are always allocated on the stack, just like i32 values.
  2. The following Rust code is intended to take a String, append "&Ms", and then print the modified original string. It fails to compile for several reasons related to ownership and mutability.

    fn add_m(msg: String) {
        msg.push_str("&Ms");
    }
    
    fn main() {
        let tasty = String::from("M");
        add_m(tasty);
        println!("Yum: {}", tasty);
    }

    Task: Fix the code so it compiles and correctly prints “Yum: M&Ms”.

    Use the rust playground to test your work.

  3. In class, we wrote a function like this to create a new, pluralized version of a string without altering the original one:

    define ptr @pluralize(ptr %str) {
      ; we need n, n+1, and n+2
      %n = call i64 @strlen(ptr %str)
      %n1 = add i64 %n, 1
      %n2 = add i64 %n, 2
    
      ; allocate new heap array with length n+2
      %result = call ptr @malloc(i64 %n2)
      ; copy original string into new one
      call void @memcpy(ptr %result, ptr %str, i64 %n)
    
      ; assign the s (same as before!)
      %offn = getelementptr i8, ptr %result, i64 %n
      store i8 115, ptr %offn
      %offn1 = getelementptr i8, ptr %result, i64 %n1
      store i8 0, ptr %offn1
    
      ret ptr %result
    }

    Question: Why is heap allocation with @malloc necessary in this version? (Select all that apply)

    1. If the new string were allocated on the stack with alloca, the memory would be automatically deallocated when the pluralize function returns.
    2. Returning a pointer to memory allocated with alloca from within a function results in a dangling pointer.
    3. @malloc is the only instruction that can allocate memory for an array of i8 characters.
    4. The load and store instructions only work on pointers returned from @malloc, not alloca.
    5. The new string’s memory must “live” longer than the function call that created it, which is the purpose of heap allocation.
  4. Why does the introduction of branching (like if/else or loops) make stack memory (alloca/load/store) necessary, even for simple variables? (Select all that apply)

    1. LLVM is an SSA (Static Single Assignment) language, meaning a register (e.g., %x) cannot be assigned more than once.
    2. A loop, by definition, needs to update a variable (like a counter i). In SSA, this changing value must be stored in and re-loaded from memory.
    3. In an if/else structure, both branches might assign a different value to the same variable.
    4. To resolve the if/else problem, both the ifblock and elseblock must store their computed value to a single memory address (%zaddr) allocated in the top block.
    5. The br instruction requires all registers to be cleared, so values must be saved to the stack first.
  5. Which of the following statements about LLVM basic blocks and branching are correct? (Select all that apply)

    1. A basic block is a sequence of instructions that ends with a terminator instruction, such as br or ret.
    2. The conditional br instruction must specify both an “if” destination (label %iflabel) and an “else” destination (label %elselabel).
    3. If a conditional br instruction’s condition is false, execution “falls through” to the next instruction after the br.
    4. If there is no label at the beginning of a function, then that function does not have any basic blocks.
    5. A single basic block can contain multiple br instructions to create complex internal logic before the terminator.
  6. Translate the following C function into LLVM IR. Your translation must use the alloca/load/store pattern to manage the variables i and sum, as their values change.

    int sum_up_to(int n) {
      int i = 1;
      int sum = 0;
      while (i <= n) {
        sum = sum + i;
        i = i + 1;
      }
      return sum;
    }
  7. Draw the control flow graph (CFG) for the LLVM function you wrote for the previous problem.

  8. Which of the following statements accurately describe a closure? (Select all that apply)

    1. A closure is a “bundle” containing a function’s code plus its captured referencing environment.
    2. The captured variables in a closure are resolved based on where the function is called (dynamic scope).
    3. The captured variables in a closure are resolved based on where the function was defined (lexical scope).
    4. In Java, a lambda that captures a local variable is conceptually similar to creating an object instance that stores the captured data and has one method.
    5. A Rust lambda that captures a local variable can lead to a dangling reference if it is called after the captured variable’s stack frame is deallocated