How to implement doubly linked list in ReScript

I don’t know if my implementation here makes sense

type rec dNode =
  | DNode({value: string, mutable left: dNode, mutable right: dNode})
  | Empty

let rec push = (list, node) => {
  switch list {
  | DNode({right: DNode(_) as right}) => push(right, node)
  | DNode({right: Empty} as curr) => curr.right = node
  | Empty => ()
  }
}

let rec unshift = (list, node) => {
  switch list {
  | DNode({left: DNode(_) as left}) => unshift(left, node)
  | DNode({left: Empty} as curr) => curr.left = node
  | Empty => ()
  }
}

One issue with your implementation is that empty lists cannot be updated. A way to solve this is to make the node a record instead of a variant, and wrap it in an option. Here’s one possible implementation similar to yours:

type rec node<'a> = {
  value: 'a,
  mutable left: option<node<'a>>,
  mutable right: option<node<'a>>,
}

type t<'a> = ref<option<node<'a>>>

let make = () => ref(None)

// Add an element after the right-most end of the list.
let push = (t, x) =>
  switch t.contents {
  // If the list is empty, update it with a singleton.
  | None => t := Some({value: x, left: None, right: None})
  | Some(node) =>
    // Loop through all of the "right" nodes until we reach the end.
    let rec aux = node =>
      switch node {
      | {right: Some(node)} => aux(node)
      | {right: None} =>
        let new_node = {value: x, left: Some(node), right: None}
        node.right = Some(new_node)
      }
    aux(node)
  }

This push is not very efficient though, since it has to transverse the entire list before adding the value. In practice, it may be better to just push the value to the immediate “right” (or “left”) of the root node, and then set that as the new root:

let push = (t, x) =>
  switch t.contents {
  | None => t := Some({value: x, left: None, right: None})
  | Some(node) =>
    // Insert the new node between the current root node and it's "right" neighbor.
    let new_node = {value: x, left: Some(node), right: node.right}
    node.right = Some(new_node)
    // Set the new node as the root.
    t := Some(new_node)
  }

How exactly you implement these functions will depend on how you need to use them, though.