aboutsummaryrefslogtreecommitdiffstats
open Core
open Types

(* The Nix elaborator does a few things:
   - Attribute paths are transformed into a simple list of expressions:
     + Simple identifiers are rewritten to string values
     + Antiquotations are rewritten to their component expressions
     + Anything else, that is not a string value, is rejected
       and raises an exception
   - In 'inherit (...) x1 ... xn', x1 ... xn are checked for 'reasonably' being
     identifiers, i.e., being one of x, "x" and ${"x"}.
   - Nested attribute paths are unfolded and attribute sets are merged where
     possible. (Where we mean 'what Nix does' with 'where possible'; see the
     comment at the respective function.)
   - Paths are turned into strings and made absolute w.r.t. the current
     working directory.
   - Indented strings are converted to their 'normal' counterpart. *)

exception ElaborateError of string

type attr_set = recursivity * binding list

let set_expr (r, bs) = Val (AttSet (r, bs))
let get_id = function Id x -> x | _ -> assert false

let rec update_bnd (bs : binding list) (x : string) ~(f : expr option -> expr) =
  match bs with
  | [] -> [ AttrPath ([ Val (Str (x, [])) ], f None) ]
  | AttrPath ([ Val (Str (y, [])) ], e) :: s' when String.(x = y) ->
      AttrPath ([ Val (Str (y, [])) ], f (Some e)) :: s'
  | Inherit (_, ids) :: _
    when List.exists ids ~f:(fun e -> String.(get_id e = x)) ->
      raise (ElaborateError "Cannot update inherit")
  | bnd :: s' -> bnd :: update_bnd s' x ~f

let set_update_bnd (r, bs) x ~f = (r, update_bnd bs x ~f)

let rec has_bnd (bs : binding list) (x : string) : bool =
  match bs with
  | [] -> false
  | AttrPath ([ Val (Str (y, [])) ], _) :: _ when String.(x = y) -> true
  | Inherit (_, ids) :: _
    when List.exists ids ~f:(fun e -> String.(get_id e = x)) ->
      true
  | _ :: bs' -> has_bnd bs' x

let merge_bnds bs1 bs2 : binding list =
  List.fold_left bs2 ~init:bs1 ~f:(fun bs1' b2 ->
      match b2 with
      | AttrPath ([ Val (Str (x, [])) ], e) ->
          update_bnd bs1' x ~f:(function
            | Some _ -> raise (ElaborateError "Duplicated attribute")
            | None -> e)
      | AttrPath ([ d ], e) -> AttrPath ([ d ], e) :: bs1'
      | Inherit (md, xs) ->
          if List.for_all xs ~f:(fun e -> not (has_bnd bs1' (get_id e))) then
            Inherit (md, xs) :: bs1'
          else raise (ElaborateError "Duplicated attribute")
      | _ -> assert false)

(* This function intentionally clobbers recursivity, because that is the way
   that Nix likes to handle attribute insertion. See
     (1) https://github.com/NixOS/nix/issues/9020
     (2) https://github.com/NixOS/nix/issues/11268
     (3) https://github.com/NixOS/nix/pull/11294 *)
let rec insert (bs : binding list) (path : expr list) (e : expr) =
  match path with
  | [] -> raise (ElaborateError "Cannot insert attribute with empty path")
  | [ Val (Str (x, [])) ] ->
      update_bnd bs x ~f:(function
        | None -> e
        | Some (Val (AttSet (r1, bs1))) -> (
            match e with
            | Val (AttSet (_, bs2)) -> set_expr (r1, merge_bnds bs1 bs2)
            | _ -> raise (ElaborateError "Duplicated attribute"))
        | _ -> raise (ElaborateError "Duplicated attribute"))
  | Val (Str (x, [])) :: rest ->
      update_bnd bs x ~f:(function
        | Some (Val (AttSet (r, bs))) -> Val (AttSet (r, insert bs rest e))
        | Some _ -> raise (ElaborateError "Duplicated attribute")
        | None -> Val (AttSet (Nonrec, insert [] rest e)))
  | [ part ] -> AttrPath ([ part ], e) :: bs
  | part :: rest ->
      AttrPath ([ part ], Val (AttSet (Nonrec, insert [] rest e))) :: bs

let insert_inherit (bs : binding list) (from : expr option) (es : expr list) =
  if List.for_all es ~f:(fun e -> not (has_bnd bs (get_id e))) then
    Inherit (from, es) :: bs
  else raise (ElaborateError "Duplicated attribute")

let simplify_path_component = function
  | Id x -> Val (Str (x, []))
  | Val (Str (s, ess)) -> Val (Str (s, ess))
  | Aquote e -> e
  | _ -> raise (ElaborateError "Unexpected path component")

let simplify_path = List.map ~f:simplify_path_component

let simplify_bnd_paths =
  List.map ~f:(fun bnd ->
      match bnd with
      | AttrPath (path, e) -> AttrPath (simplify_path path, e)
      | Inherit (me, xs) -> Inherit (me, xs))

(* Law: concat_lines ∘ split_lines = id *)

let rec split_lines s =
  match String.lsplit2 s ~on:'\n' with
  | Some (s1, s2) -> s1 :: split_lines s2
  | None -> [ s ]

let rec concat_lines = function
  | [] -> ""
  | [ x ] -> x
  | x :: xs -> x ^ "\n" ^ concat_lines xs

let map_tail ~f = function [] -> [] | x :: xs -> x :: List.map ~f xs

let unindent n s ~skip_first_line =
  let map_op ~f = if skip_first_line then map_tail ~f else List.map ~f in
  split_lines s
  |> map_op ~f:(fun line ->
         let expected_prefix = String.make n ' ' in
         String.chop_prefix_if_exists ~prefix:expected_prefix line)
  |> concat_lines

let is_spaces l = String.(strip l ~drop:(Char.( = ) ' ') |> is_empty)

let drop_first_empty_line s =
  match String.lsplit2 s ~on:'\n' with
  | Some (l, s') when is_spaces l -> s'
  | _ -> s

let rec process ?(dir = None) = function
  | BinaryOp (op, e1, e2) -> BinaryOp (op, process ~dir e1, process ~dir e2)
  | UnaryOp (op, e) -> UnaryOp (op, process ~dir e)
  | Cond (e1, e2, e3) -> Cond (process ~dir e1, process ~dir e2, process ~dir e3)
  | With (e1, e2) -> With (process ~dir e1, process ~dir e2)
  | Assert (e1, e2) -> Assert (process ~dir e1, process ~dir e2)
  | Test (e1, es) ->
      Test (process ~dir e1, List.(simplify_path es >>| process ~dir))
  | SetLet bs -> SetLet (process_bnds ~dir bs)
  | Let (bs, e) -> Let (process_bnds ~dir bs, process ~dir e)
  | Val v -> Val (process_val ~dir v)
  | Id x -> Id x
  | Select (e, es, me) ->
      Select
        ( process ~dir e,
          List.(simplify_path es >>| process ~dir),
          Option.(me >>| process ~dir) )
  | Apply (e1, e2) -> Apply (process ~dir e1, process ~dir e2)
  | Aquote e -> Aquote (process ~dir e)

and process_val ~dir = function
  | Str (s, ess) -> Str (s, List.(ess >>| fun (e, s) -> (process ~dir e, s)))
  | IStr (n, s, ess) ->
      let s' = drop_first_empty_line (unindent n s ~skip_first_line:false)
      and ess' =
        List.map ess ~f:(fun (e, s) ->
            (process ~dir e, unindent n s ~skip_first_line:true))
      in
      Str (s', ess')
  | Lambda (p, e) -> Lambda (process_pattern ~dir p, process ~dir e)
  | List es -> List List.(es >>| process ~dir)
  | AttSet (r, bs) -> AttSet (r, process_bnds ~dir bs)
  | Path p -> (
      if Filename.is_absolute p then Str (p, [])
      else
        match dir with
        | Some dir when Filename.is_absolute dir ->
            Str (Filename.concat dir p, [])
        | Some _ ->
            raise
              (ElaborateError "Provided directory should be an absolute path")
        | None -> raise (ElaborateError "Do not know how to resolve path"))
  | v -> v

and process_bnds ~dir bs =
  bs
  |> List.map ~f:(function
       | AttrPath (es, e) ->
           AttrPath (List.(es >>| process ~dir), process ~dir e)
       | Inherit (me, xs) ->
           Inherit (Option.(me >>| process ~dir), process_inherit_ids xs))
  |> simplify_bnd_paths
  |> List.fold ~init:[] ~f:(fun bs' bnd ->
         match bnd with
         | AttrPath (path, e) -> insert bs' path e
         | Inherit (from, es) -> insert_inherit bs' from es)

and process_inherit_ids =
  List.map ~f:(function
    | Id x | Val (Str (x, [])) | Aquote (Val (Str (x, []))) -> Id x
    | _ -> raise (ElaborateError "Unexpected expression in inherit"))

and process_pattern ~dir = function
  | Alias x -> Alias x
  | ParamSet (mx, (ps, k)) -> ParamSet (mx, (process_param_set ~dir mx ps, k))

and process_param_set ~dir ?(seen = String.Set.empty) mx ps =
  match ps with
  | [] -> []
  | (y, me) :: ps' ->
      if Set.mem seen y || Option.mem mx y ~equal:String.( = ) then
        raise (ElaborateError "Duplicated function argument")
      else
        (y, Option.(me >>| process ~dir))
        :: process_param_set ~dir mx ps' ~seen:(Set.add seen y)