Revision bc545429ddf8494c22cc1458dce1233fc53780ba authored by Diane Gallois-Wong on 14 June 2023, 12:40:14 UTC, committed by Diane Gallois-Wong on 15 June 2023, 11:50:07 UTC
1 parent 1c6c675
Raw File
michelson_script.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 Nomadic Labs <contact@nomadic-labs.com>                *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

let default_prefix = "michelson_test_scripts"

type version_range = {range_start : int; range_end : int option}

let unversioned_range = {range_start = 0; range_end = None}

let in_range {range_start; range_end} protocol =
  let n = Protocol.number protocol in
  let range_end = Option.value range_end ~default:Int.max_int in
  range_start <= n && n <= range_end

type t = {
  prefix : string;
  dirname : string list;
  basename : string;
  name : string;
  version_range : version_range;
  depth : int;
}

let pretty_string {prefix; dirname; basename; name; version_range; depth} =
  let option_to_string f opt =
    match opt |> Option.map f with None -> "None" | Some s -> "Some " ^ s
  in
  String.concat
    "\n"
    [
      "{";
      "  prefix = " ^ prefix;
      "  dirname = " ^ String.concat Filename.dir_sep dirname;
      "  basename = " ^ basename;
      "  name = " ^ name;
      "  version_range = ";
      "  {";
      "    range_start = " ^ string_of_int version_range.range_start;
      "    range_end = "
      ^ option_to_string string_of_int version_range.range_end;
      "  }";
      "  depth = " ^ string_of_int depth;
      "}";
    ]

let parse_basename : string -> (string * version_range) option =
  let re3 = rex "(.*)_([0-9]{3,})_([0-9]{3,})\\.tz" in
  let re2 = rex "(.*)_([0-9]{3,})\\.tz" in
  let re1 = rex "(.*).tz" in
  fun s ->
    match s =~*** re3 with
    | Some (name, range_start, range_end) ->
        let version_range =
          {
            range_start = int_of_string range_start;
            range_end = Some (int_of_string range_end);
          }
        in
        Some (name, version_range)
    | None -> (
        match s =~** re2 with
        | Some (name, range_start) ->
            let version_range =
              {range_start = int_of_string range_start; range_end = None}
            in
            Some (name, version_range)
        | None -> (
            match s =~* re1 with
            | Some name -> Some (name, unversioned_range)
            | None -> None))

let memoize f =
  let cache = Hashtbl.create 16 in
  fun key ->
    match Hashtbl.find_opt cache key with
    | Some res -> res
    | None ->
        let res = f key in
        Hashtbl.add cache key res ;
        res

let walk (prefix, maxdepth) =
  let rec loop depth dirname =
    if depth >= maxdepth then []
    else
      List.fold_left ( // ) prefix dirname
      |> Sys.readdir |> Array.to_list
      |> List.concat_map @@ fun basename ->
         let dirname' = dirname @ [basename] in
         let dirname_s' = List.fold_left ( // ) prefix dirname' in
         if Sys.is_directory dirname_s' then loop (depth + 1) dirname'
         else
           match parse_basename basename with
           | None -> []
           | Some (name, version_range) ->
               [{prefix; dirname; basename; name; version_range; depth}]
  in
  loop 1 [] |> List.sort (Stdlib.compare : t -> t -> int)

let walk =
  let f = memoize walk in
  fun ?(prefix = default_prefix) ?(maxdepth = Int.max_int) () ->
    f (prefix, maxdepth)

let path ?(no_prefix = false) t =
  if no_prefix then String.concat Filename.dir_sep t.dirname // t.basename
  else t.prefix // String.concat Filename.dir_sep t.dirname // t.basename

let split_name name =
  match List.rev name with
  | [] -> Error (__LOC__, "name must not be an empty list")
  | last :: dirname_rev -> Ok (List.rev dirname_rev, last)

let name t = t.dirname @ [t.name]

let name_s t = name t |> String.concat "/"

(** A private helper type for use in [choose_versioned]. Describes a script
    being either unversioned (such as foo.tz), or versioned (such as
    foo_NNN.tz or foo_NNN_MMM.tz) *)
type kind = Versioned | Unversioned

exception Test_fail of string * string

(** [choose_versioned t1 t2] decides which of [t1] or [t2] to keep. It assumes
    that both [t1] and [t2] have already been checked against a protocol, and
    that the protocol falls within their ranges.

    If [t1] and [t2] have a different [kind], then the Versioned one is always
    chosen. Consider:
    - foo.tz
    - foo_015.tz

    Assuming the protocol we care about is 015, and we look at each version range
    in isolation, then we have:
    - foo.tz (0 to infinity)
    - foo_015.tz (015 to infinity)

    So we can say foo_015.tz supercedes foo.tz and the version_ranges should
    actually be:
    - foo.tz (0 to 014)
    - foo_015.tz (015 to infinity)

    Next, if both [t1] and [t2] are versioned, then we do a few checks:
    - If the [range_start]s are the same, then we cannot disambiguate them.

      For example:
      - foo_014.tz (014 to infinity)
      - foo_014_015.tz (014 to 015)

      If we are looking for protocol 015, then we cannot decide between the two,
      so we return an error.
    - If the [range_start]s are different, then we determine which one has the higher
      range_start.

      For example, assume the following:
      - foo_014.tz (014 to infinity)
      - foo_015.tz (015 to infinity)

      If we are searching for protocol 015, both files are individually in
      range. If the end range of the smaller one is infinity, then we say that
      the larger range_start supercedes the smaller one, and we get:
      - foo_014.tz (014 to 014)
      - foo_015.tz (015 to infinity)
    - If the smaller range_start has a range_end which is > the larger scripts'
      range_start, then we cannot disambiguate.

      For example, assume the following:
      - foo_014_015.tz
      - foo_015.tz

      If we are looking for protocol 015, then we cannot say which one of the
      scripts is the correct one, so we return an error. *)
let choose_versioned_exn t1 t2 ~protocol =
  let kind v = if v = unversioned_range then Unversioned else Versioned in
  let res =
    match (kind t1.version_range, kind t2.version_range) with
    | Versioned, Unversioned -> Ok t1
    | Unversioned, Versioned -> Ok t2
    | Unversioned, Unversioned ->
        (* This case should never happen. It would indicate that we found two
           unversioned scripts with the same name, such as foo.tz and foo.tz. *)
        Error Unversioned
    | Versioned, Versioned -> (
        let s1 = t1.version_range.range_start in
        let s2 = t2.version_range.range_start in
        if s1 = s2 then Error Versioned
        else
          let tsmall, tlarge = if s1 < s2 then (t1, t2) else (t2, t1) in
          match tsmall.version_range.range_end with
          | None -> Ok tlarge
          | Some e1 ->
              if e1 < tlarge.version_range.range_start then Ok tlarge
              else Error Versioned)
  in
  match res with
  | Ok x -> x
  | Error kind ->
      let kind =
        match kind with
        | Versioned -> "versioned"
        | Unversioned -> "unversioned"
      in
      raise
      @@ Test_fail
           ( __LOC__,
             sf
               "found more than one %s match for (name: %s) and (protocol: %03d)\n\
               \ - %s\n\
               \ - %s"
               kind
               (name_s t1)
               (Protocol.number protocol)
               (path ~no_prefix:true t1)
               (path ~no_prefix:true t2) )

let find_all_res (prefix, maxdepth, protocol) =
  (* Group by name and reduce matches down to just one. *)
  try
    walk ~prefix ~maxdepth ()
    |> List.fold_left
         (fun acc t ->
           if not (protocol |> in_range t.version_range) then acc
           else
             String_map.update
               (name_s t)
               (function
                 | None -> Some t
                 | Some t' -> Some (choose_versioned_exn t t' ~protocol))
               acc)
         String_map.empty
    |> String_map.bindings |> List.map snd |> Result.ok
  with Test_fail (__LOC__, msg) -> Error (__LOC__, msg)

let find_all_res =
  let f = memoize find_all_res in
  fun ?(prefix = default_prefix) ?(maxdepth = Int.max_int) protocol ->
    f (prefix, maxdepth, protocol)

let ok_or_fail = function
  | Ok x -> x
  | Error (__LOC__, msg) -> Test.fail ~__LOC__ "%s" msg

let find_all ?prefix ?maxdepth protocol =
  find_all_res ?prefix ?maxdepth protocol |> ok_or_fail

let find_res (prefix, maxdepth, name, protocol) =
  let ( let* ) = Result.bind in
  let* dirname, name = split_name name in
  let* v = find_all_res ~prefix ~maxdepth protocol in
  match v |> List.find_opt @@ fun t -> t.dirname = dirname && t.name = name with
  | Some t -> Ok t
  | None ->
      let dirname_s = String.concat Filename.dir_sep dirname in
      let version = Protocol.number protocol in
      Error
        ( __LOC__,
          sf
            "could not find Michelson script %S for protocol %03d in %s: found \
             no file named %s_NNN.tz such that 000 <= NNN <= %03d; found no \
             file named %s_NNN_MMM.tz such that 000 <= %03d <= MMM; found no \
             unversioned file named %s.tz"
            (dirname_s // name)
            version
            prefix
            name
            version
            name
            version
            name )

let find_res =
  let f = memoize find_res in
  fun ?(prefix = default_prefix) ?(maxdepth = Int.max_int) name protocol ->
    f (prefix, maxdepth, name, protocol)

let find ?prefix ?maxdepth name protocol =
  find_res ?prefix ?maxdepth name protocol |> ok_or_fail

let blacklist dirs ts =
  ts
  |> List.filter @@ fun (t : t) ->
     not @@ List.exists (fun dir -> List.equal String.equal dir t.dirname) dirs

let whitelist dirs ts =
  ts
  |> List.filter @@ fun (t : t) ->
     List.exists (fun dir -> List.equal String.equal dir t.dirname) dirs

let find_all_in ?prefix ?maxdepth protocol dirs =
  find_all ?prefix ?maxdepth protocol |> whitelist [dirs]

let find_all_legacy ?prefix ?maxdepth protocol =
  find_all ?prefix ?maxdepth protocol |> whitelist [["legacy"]]

let find_all_well_typed ?prefix ?maxdepth protocol =
  find_all ?prefix ?maxdepth protocol |> blacklist [["ill_typed"]; ["legacy"]]

let find_all_ill_typed ?prefix ?maxdepth protocol =
  find_all ?prefix ?maxdepth protocol |> whitelist [["ill_typed"]]
back to top