#! /usr/bin/env bash
#
# Generate OCaml parsers
#
# For lang=ruby, the file hierarchy we create looks like this:
#
# ocaml-src
# ├── bin
# │   ├── dune
# │   └── Main.ml
# ├── config
# │   ├── dune
# │   ├── discover.ml
# ├── lib
# │   ├── bindings.c
# │   ├── Boilerplate.ml
# │   ├── CST.ml
# │   ├── dune
# │   ├── Parse.ml
# │   ├── Parse.mli
# │   ├── parser.c
# │   ├── scanner.cc
# │   └── tree_sitter
# │       └── parser.h
# └── tree-sitter-lang.opam
#
set -eu -o pipefail

default_dst_dir="ocaml-src"
default_src_dir="src"
default_lang="lang"
default_package="tree-sitter-lang"

usage() {
  cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Call ocaml-tree-sitter to derive an OCaml parsing library and executable
from a tree-sitter grammar in json format, normally generated by
tree-sitter as 'src/grammar.json'.

Options:
  --dst DST_DIR
      Specify the output directory. Default: $default_dst_dir
  --help
      Show this help message and exit.
  --lang LANG
      Name of the programming language. It will be part of the name
      of the library SUPERNAME.LANG and of the OCaml module.
      Case conversion and conversions between dashes and underscores will
      take place as needed.
      Default: $default_lang
  --src SRC_DIR
      Location of the 'src' folder with some of its contents generated
      by tree-sitter. It must contain 'grammar.json', 'parser.c',
      and optionally 'scanner.c' needed to build the C parser. C++ scanners
      are not supported. Default: $default_src_dir
  --package PKG
      The Dune package name to which the library will belong.
      The library name will be PKG.LANG where LANG is the language name.
      Default: $default_package.
  --trace
      Print debugging info during parsing.
EOF
}

error() {
  cat >&2 <<EOF
Error: $*
EOF
  exit 1
}

lang="$default_lang"
package="$default_package"
src_dir="$default_src_dir"
dst_dir="$default_dst_dir"
trace_option=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --dst)
      dst_dir="$2"
      shift
      ;;
    --help)
      usage
      exit 0
      ;;
    --lang)
      lang="$2"
      shift
      ;;
    --package)
      package="$2"
      shift
      ;;
    --src)
      src_dir="$2"
      shift
      ;;
    --trace)
      trace_option="--trace"
      ;;
    *)
      error "Unsupported argument: $1"
  esac
  shift
done

ots_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
ocaml_tree_sitter="$ots_root/core/bin/ocaml-tree-sitter"
[[ -x "$ocaml_tree_sitter" ]] || error "Run make grammar-tools from the Semgrep repository root."

lang_dashes=$(echo "$lang" | tr 'A-Z_' 'a-z-')
lang_underscores=$(echo "$lang" | tr 'A-Z-' 'a-z_')

# Copy what we need to the ocaml-src folder. We need at least to copy
# the C source and headers.
#
rm -rf "$dst_dir"
mkdir -p "$dst_dir"/lib
mkdir -p "$dst_dir"/config

# Build the list of C files to compile, without their extension.
#
c_files="parser bindings"

cp "$src_dir"/parser.c "$dst_dir"/lib

if [[ -e "$src_dir"/scanner.c ]]; then
  cp "$src_dir"/scanner.c "$dst_dir"/lib
  c_files="scanner $c_files"
fi

if [[ -e "$src_dir"/scanner.cc ]]; then
  echo "$(basename "$0"): C++ scanners are not supported: $src_dir/scanner.cc" >&2
  exit 1
fi

# Copy C header files
#
shopt -s nullglob
for x in "$src_dir"/*.h; do cp "$x" "$dst_dir"/lib; done
cp -a "$src_dir"/tree_sitter "$dst_dir"/lib/tree_sitter

# Generate the OCaml code needed to parse the examples/*.out json files.
#
"$ocaml_tree_sitter" \
  gen \
  "$lang_underscores" \
  "$src_dir"/grammar.json \
  -d "$dst_dir" \
  "${trace_option[@]}"

cat > "$dst_dir"/lib/bindings.c <<EOF
/*
  Generated by ocaml-tree-sitter for $lang_underscores.
*/

#include <string.h>
#include <tree_sitter/api.h>

#include <caml/alloc.h>
#include <caml/bigarray.h>
#include <caml/callback.h>
#include <caml/custom.h>
#include <caml/fail.h>
#include <caml/memory.h>
#include <caml/mlvalues.h>
#include <caml/threads.h>

// Implemented by parser.c
const TSLanguage *tree_sitter_${lang_underscores}(void);

typedef struct _parser {
  TSParser *parser;
} parser_W;

static void finalize_parser(value v) {
  parser_W *p;
  p = (parser_W *)Data_custom_val(v);
  ts_parser_delete(p->parser);
}

static struct custom_operations parser_custom_ops = {
  .identifier = "parser handling",
  .finalize = finalize_parser,
  .compare = custom_compare_default,
  .hash = custom_hash_default,
  .serialize = custom_serialize_default,
  .deserialize = custom_deserialize_default
};

// OCaml function
CAMLprim value octs_create_parser_${lang_underscores}(value unit) {
  (void)unit;
  CAMLparam0();
  CAMLlocal1(v);

  TSParser *parser = ts_parser_new();

  // Fail loudly on an ABI version mismatch: ts_parser_set_language returns
  // false (and assigns no language) when the compiled grammar's ABI version
  // is outside the linked runtime's supported range. Raising here -- before
  // allocating the boxed parser value -- avoids silently handing back a
  // parser that yields empty parse trees, and keeps the cleanup path
  // unambiguous (no half-initialized custom block for the finalizer to see).
  if (!ts_parser_set_language(parser, tree_sitter_${lang_underscores}())) {
    ts_parser_delete(parser);
    caml_failwith(
      "ts_parser_set_language failed for ${lang_underscores}: the compiled "
      "grammar's ABI version is incompatible with the linked tree-sitter "
      "runtime");
  }

  parser_W parserWrapper;
  parserWrapper.parser = parser;

  v = caml_alloc_custom(&parser_custom_ops, sizeof(parser_W), 0, 1);
  memcpy(Data_custom_val(v), &parserWrapper, sizeof(parser_W));
  CAMLreturn(v);
}
EOF

cat > "$dst_dir"/config/dune <<EOF
(executable
 (name discover)
 (libraries dune.configurator))
EOF

cat > "$dst_dir"/config/discover.ml <<EOF
(* This was adapted from the dune manual at
   https://dune.readthedocs.io/en/stable/quick-start.html
*)
module C = Configurator.V1

let () =
  C.main ~name:"configure-c-flags" (fun c ->
    let c_library_flags =
      (* The -rpath option tells the linker to hardcode this search location in
         the binary.
         This works as long as libtree-sitter stays where it is, which is fine
         for test executables. Production executables should instead link
         statically against libtree-sitter to avoid problems in locating the
         library at runtime. *)
      match C.ocaml_config_var c "os_type" with
      | Some "Win32" -> ["()"] (* Compilation on Windows does not support rpath *)
      | _ -> ["(-Wl,-rpath,%{env:TREESITTER_LIBDIR=/usr/local/lib})"]
    in
    C.Flags.write_lines "c_library_flags.sexp" c_library_flags)
EOF

cat > "$dst_dir"/lib/dune <<EOF
; required to install tree_sitter/parser.h
(include_subdirs qualified)

(library
  (public_name $package.${lang_dashes})
  (name tree_sitter_${lang_underscores})
  (libraries atdgen-runtime tree-sitter.run)

  ; A copy of the C headers for the tree-sitter library is found locally.
  ; This is because it's important to use the right version of 'parser.h'.
  ;
  (foreign_stubs
    (language c)
    (names ${c_files})
    (flags -std=c11
           -fPIC
           -I %{env:TREESITTER_INCDIR=/usr/local/include}
           -I .)
  )

  ; TREESITTER_LIBDIR is meant to be some nonstandard location containing the
  ; desired version of the tree-sitter library.
  ; The -rpath option tells the linker to hardcode this search location
  ; in the binary.
  ;
  ; Including the c_library_flags.sexp allows customizing the linker arguments
  ; based on the platform we build on.
  ;
  (c_library_flags
    (
      -L%{env:TREESITTER_LIBDIR=/usr/local/lib}
      -ltree-sitter
      (:include c_library_flags.sexp)
    )
  )
)

(rule
 (targets c_library_flags.sexp)
 (action (run ../config/discover.exe)))
EOF

cat > "$dst_dir"/bin/dune <<EOF
(executable
  (package $package)
  (public_name parse-${lang_dashes})
  (name Main)
  (libraries $package.${lang_dashes})
)
EOF

# .gitignore file suitable for building with dune.
cat > "$dst_dir"/.gitignore <<EOF
# Dune build data
_build

# Merlin files for Vim and Emacs generated by dune < 2.8
.merlin

# Dune-generated files
*.install
EOF

############################################################################

# The following files are needed to build the libraries and executables
# as a standalone dune project. This is for local testing.
#
# We don't include these files in the destination git repo so it can be
# plugged into an existing git project without modifications.

cat > "$dst_dir"/dune-project <<EOF
(lang dune 3.7)
(name tree-sitter-$lang)
EOF

# This is needed for building with dune.
#
touch "$dst_dir"/tree-sitter-lang.opam
