Use scala 3. Replace the ??? symbols with your solutions.
package shallowOps:

  import tinylog._

  object factory:

    /**
    * A helper method implementing a half adder.
    * @return a pair (c_out, s), where
    * (i)  c_out evaluates to true iff both argument gates are true, and
    * (ii) s evaluates to true iff an odd number of inputs are true.
    *
    */
    protected def buildHalfAdder(a: Gate, c_in: Gate): (Gate, Gate) =
      val c_out = a && c_in
      val s = (a || c_in) && !c_out
      (c_out, s)
    end buildHalfAdder
    /**
    * A helper method implementing a full adder.
    * @return a pair (c_out, s), where
    * (i)  c_out is a gate evaluating to true iff at least two of the 
    *      argument gates are true, and
    * (ii) s is a gate evaluating to true iff an odd number of inputs are true.
    *
    */
    protected def buildFullAdder(a: Gate, b: Gate, c_in: Gate): (Gate, Gate) =
      val c_out = (!a && b && c_in) || (a && !b && c_in) || (a && b && !c_in) || (a && b && c_in)
      val s = (!a && !b && c_in) || (!a && b && !c_in) || (a && !b && !c_in) || (a && b && c_in)
      (c_out, s)
    end buildFullAdder
    /**
    * A simple ripple carry adder with modulo semantics.
    * Returns a bus that evaluates to the sum of values of the argument buses 
    * (modulo 2^n, where n is the bus length).
    *
    * For instance, assume that aa and bb are buses with 3 bits.
    * When aa evaluates to 011 (3 in decimal) and bb to 110 (6 in decimal),
    * then the bus adder(aa, bb) should evaluate to 001 (9 mod 8 in decimal).
    *
    */
    def buildAdder(a: Bus, b: Bus): Bus =
      require(a.length == b.length, "The buses must be of the same length")
      require(a.length > 0, "Cannot build an adder on an empty bus")
      val n = a.length
      var carry_in: Gate = Gate.False // no initial carry
      val ss = new Array[Gate](n)
      for i <- 0 until n do
        val (carry_out, sum) = buildFullAdder(a(i), b(i), carry_in)
        carry_in = carry_out // carry from bit i propagates to bit i+1
        ss(i) = sum
      new Bus(ss.toIndexedSeq)
    end buildAdder
    /**
    * Task 1: Logarithmic-depth OR
    *
    * This task initiates you to the design and implementation of circuits
    * with constrained depth and size. More precisely, you are to design
    * a circuit that computes the OR of an n-bit input bus. That is, you
    * are to design a circuit that outputs a single bit, which must be
    * false if and only if all the n input bits are false; otherwise
    * the output must be true.
    *
    * The design constraints are that, for all n=1,2,3,..., your circuit
    * must have:
    *
    * (i)  Depth at most the base-2 logarithm of n, rounded up.
    *      For example, for n=1 the depth must be 0, for n=2 the depth
    *      must be 1, for n=4 the depth must be 2, for n=1024 the depth
    *      must be 10, and so forth.
    *
    * and
    *
    * (ii) Size at most 2*n-1.
    *
    * Hint:
    * Structure your circuit as a perfect binary tree when n is equal
    * to a power of 2.
    *
    */
    def buildShallowOr(a: Bus): Gate =
      require(a.length > 0, "The bus cannot be empty")
      ???
    end buildShallowOr

    /**
    * Task 2: Logarithmic-depth incrementer.
    *
    * This task asks you to design an incrementer circuit with constrained
    * depth and size. That is, a circuit that takes an n-bit bus as input, and
    * outputs an n-bit bus whose value is equal to the least significant
    * n bits of the input plus one.
    *
    * The design constraints are that, for all n=2,3,4,..., your circuit
    * must have:
    *
    * (i) Depth at most 4*log2(n), where log2(n) is the base-2 logarithm of n.
    *
    * and
    *
    * (ii) Size at most 10*n*log2(n).
    *
    */
    def buildShallowIncrementer(in: Bus): Bus =
      require(in.length > 0, "Cannot build an incrementer on an empty bus")
      ???
    end buildShallowIncrementer
    
    /** 
    * Task 3: Logarithmic-depth adder. 
    *
    * This task asks you to design an adder circuit with constrained
    * depth and size. That is, a circuit that takes two n-bit buses as input, 
    * and outputs an n-bit bus whose value is equal to the least significant
    * n bits of the sum of the two inputs.
    *
    * The design constraints are that, for all n=2,3,4,..., your circuit 
    * must have:
    *
    * (i)  Depth at most 6*log2(n), where log2(n) is the base-2 logarithm of n.
    *
    * and
    *
    * (ii) Size at most 30*n*log2(n).
    *
    */
    def buildShallowAdder(a: Bus, b: Bus): Bus =
      require(a.length == b.length, "The buses must be of the same length")
      require(a.length > 0, "Cannot build an adder on an empty bus")
      ???
    end buildShallowAdder

  end factory

Use these classes from tinylog if needed:

package tinylog
import scala.collection.{SpecificIterableFactory, StrictOptimizedSeqOps, mutable}
import collection.SeqOps

sealed class Bus(gates: Seq[Gate]) extends Seq[Gate]
    with SeqOps[Gate, Seq, Bus]
    with StrictOptimizedSeqOps[Gate, Seq, Bus]:

  // Mandatory implementation of `apply` in SeqOps
  def apply(idx: Int) = gates.apply(idx)

  /** Creates a new Bus from a set of indexes to this one.*/
  def apply(idxs: Seq[Int]) = new Bus(idxs.map(gates(_)))

  // Mandatory implementation of `length` and `iterator`
  def length = gates.length
  def iterator = gates.iterator

  /* Operations on Gates.*/

  /** Values of Gates.*/
  def values = gates.map(_.value)

  /** The number of gates (i) in this bus and (ii) recursively referenced by the ones in this bus. */
  def nofGates: Int =
    val counted = new mutable.HashSet[Gate]()
    gates.foldLeft(0)((result, gate) => result + gate.nofReferenced(counted))

  /**
   * For a bus aa and gate g, aa && g returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) && g.
   */
  def &&(that: Gate) = new Bus(this.map(_ && that))

  /**
   * For a bus aa and gate g, aa || g returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) || g.
   */
  def ||(that: Gate) = new Bus(this.map(_ || that))

  /**
   * Bitwise negation of the bus.
   * For a bus aa, ~aa is a new bus cc such that cc(i) is !aa(i).
   */
  def unary_~ = this.map(!_)

  /**
   * Bitwise AND of two busses.
   * For two busses aa and bb, aa & bb returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) && bb(i).
   * The busses must be of the same length.
   */
  def &(that: Bus) =
    require(this.length == that.length, "Cannot take bitwise and of busses of different length")
    new Bus((this zip that).map(x => x._1 && x._2))

  /**
   * Bitwise OR of two busses.
   * For two busses aa and bb, aa | bb returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) || bb(i).
   * The busses must be of the same length.
   */
  def |(that: Bus) =
    require(this.length == that.length, "Cannot take bitwise and of busses of different length")
    new Bus((this zip that).map(x => x._1 || x._2))
  /* Because Bus is a custom collection (based on Seq) with SeqOps trait
     we need to override a a few methods so that it can inherit all of the
     standard operations from the trait while still behaving as a Bus as
     much as possible.
     If you are interested, see the RNA exampe at 
     https://docs.scala-lang.org/overviews/core/custom-collections.html#final-version-of-rna-strands-class
   */

  // Mandatory overrides of `fromSpecific`, `newSpecificBuilder`,
  // and `empty`, from `IterableOps`
  override protected def fromSpecific(coll: IterableOnce[Gate]): Bus =
    Bus.fromSpecific(coll)

  override protected def newSpecificBuilder: mutable.Builder[Gate, Bus] =
    Bus.newBuilder

  override def empty: Bus = Bus.empty

  // Overloading of `appended`, `prepended`, `appendedAll`, `prependedAll`,
  // `map`, `flatMap` and `concat` to return a `Bus` when possible
  def concat(suffix: IterableOnce[Gate]): Bus =
    strictOptimizedConcat(suffix, newSpecificBuilder)

  @inline final def ++ (suffix: IterableOnce[Gate]): Bus = concat(suffix)

  def appended(base: Gate): Bus =
    (newSpecificBuilder ++= this += base).result()

  def appendedAll(suffix: Iterable[Gate]): Bus =
    strictOptimizedConcat(suffix, newSpecificBuilder)

  def prepended(base: Gate): Bus =
    (newSpecificBuilder += base ++= this).result()

  def prependedAll(prefix: Iterable[Gate]): Bus =
    (newSpecificBuilder ++= prefix ++= this).result()

  def map(f: Gate => Gate): Bus =
    strictOptimizedMap(newSpecificBuilder, f)

  def flatMap(f: Gate => IterableOnce[Gate]): Bus =
    strictOptimizedFlatMap(newSpecificBuilder, f)

  // The class name will by default be shown as 'Seq', we don't want that.
  override def className = "Bus"


object Bus extends SpecificIterableFactory[Gate, Bus]:

  def empty: Bus = new Bus(Seq.empty)

  def newBuilder: mutable.Builder[Gate,Bus] =
    mutable.ArrayBuffer.newBuilder[Gate]
    .mapResult(s=>new Bus(s.toSeq))

  def fromSpecific(it: IterableOnce[Gate]): Bus = it match
    case seq: Seq[Gate] => new Bus(seq)
    case _ => new Bus(it.iterator.toSeq)

  /** Returns a new bus with n InputElement gates */
  def inputs(n: Int) = new Bus((1 to n).map(x => Gate.input()))
  /** Returns a new bus of n False gates */
  def falses(n: Int) = new Bus((1 to n).map(x => Gate.False))
  /** Returns a new bus of n True gates */
  def trues(n: Int) = new Bus((1 to n).map(x => Gate.True))


package tinylog

/** A class for "timestamps" */
class TimeStamp {
}

/** The abstract base class for all our Boolean gate types */
sealed abstract class Gate() 
:
  def unary_! = new NotGate(this)
  def &&(that: Gate): Gate = new AndGate(this, that)
  def ||(that: Gate): Gate = new OrGate(this, that)


  protected var memoValue: Boolean = false
  protected var memoTimeStamp: TimeStamp = null
  def value: Boolean =
    if memoTimeStamp == Gate.updatedTimeStamp then memoValue
    else
      memoValue = _eval
      memoTimeStamp = Gate.updatedTimeStamp
      memoValue
  protected def _eval: Boolean
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int] = new scala.collection.mutable.HashMap[Gate, Int]()): Int
  /**
   * The number of gates recursively referenced by this gate (including the gate itself) that are
   * not already in the set "counted".
   * The set "counted" is updated while evaluating the result.
   */
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate] = new scala.collection.mutable.HashSet[Gate]()): Int

/**
 * Companion object allowing easier construction of constant and input gates
 * */
object Gate:
  /** A "time stamp",
   * updated to indicate that an input gate has changed value
   * */
  var updatedTimeStamp = new TimeStamp()

  val False: Gate = new ConstantGate(false)
  val True: Gate  = new ConstantGate(true)
  def input() = new InputElement()

sealed class InputElement() extends Gate()
:
  var v = false                     // default value is false
  def set(s: Boolean) = {v = s; Gate.updatedTimeStamp = new TimeStamp() }
  def _eval = v
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int]) = 0
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate]) = if counted.add(this) then 1 else 0

sealed class NotGate(in: Gate) extends Gate()                
:
  def _eval = !in.value
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int]) = counted.getOrElseUpdate(this, in.depth(counted)+1)
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate]) = if counted.add(this) then in.nofReferenced(counted) + 1 else 0

sealed class OrGate(in1: Gate, in2: Gate) extends Gate()
:
  def _eval = in1.value || in2.value
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int]) = counted.getOrElseUpdate(this, (in1.depth max in2.depth)+1)
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate]) =
    if counted.add(this) then in1.nofReferenced(counted) + in2.nofReferenced(counted) + 1 else 0

sealed class AndGate(in1: Gate, in2: Gate) extends Gate()
:
  def _eval = in1.value && in2.value
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int]) = counted.getOrElseUpdate(this, (in1.depth max in2.depth)+1)
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate]) =
    if counted.add(this) then in1.nofReferenced(counted) + in2.nofReferenced(counted) + 1 else 0

sealed class ConstantGate(v: Boolean) extends Gate()
:
  def _eval = v
  def depth(implicit counted: scala.collection.mutable.Map[Gate, Int]) = 0
  def nofReferenced(implicit counted: scala.collection.mutable.Set[Gate]) = if counted.add(this) then 1 else 0