Core + jsoo-react: let%component conflict

dune file:

(library
 (name widget)
 (preprocess
  (pps jsoo_react_ppx ppx_jane))
 (libraries react js_of_ocaml brr core))

This compiles:

module Window = struct
  let t = H.div [||] []

  let%component make ~(name : string) ~(window_config : Window_config.t) = t
end

this gets compile error:

open Core
module Window = struct
  let t = H.div [||] []

  let%component make ~(name : string) ~(window_config : Window_config.t) = t
end

error:

File "ml/widget/dom.ml", line 33, characters 2-76:
33 |   let%component make ~(name : string) ~(window_config : Window_config.t) = t
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error (warning 6 [labels-omitted]): label f was omitted in the application of this function.
File "ml/widget/dom.ml", line 33, characters 2-76:
33 |   let%component make ~(name : string) ~(window_config : Window_config.t) = t
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type string -> Js.js_string Js.t
       but an expression was expected of type 'a option

What is going on, and how do I fix this ?

Thanks!

What is going on

Unlike other libraries that try to extend the standard library (e.g. containers) base and core are meant as complete replacement. And method signatures in Core can be imcompatible with the stdlib.
let%component is probably generating code relying on some function of the stdlib String module but with open Core its in fact using the Core String module and it’s uncompatible libraries. (fair guess based on the compiler error, but I haven’t check the code).

how do I fix this

Not sure you will like my suggestions (but people more expert in the subject could have better ones):

  1. Only use stdlib extentions, not replacements (so no core, or base)
  2. Do not use ppx imcompatible with the expected usage of Core.
  3. Try to isolate any none-JS ppx from the rest of your code, so you can open Core in most places without impacting the code generation.
  4. Prefix all your usages of modules that would be in stdlib with Core. and deal with the weird compiler errors if you ever forget one.

Option 3. might be the less annoying to pick if you are already invested in core and don’t fancy option 1.

2 Likes

in vscode there is a command to show the preprocessed version of a file, ie after the ppx runs. It could help you understand what is happening.

Alternatively you can use dune directly. From the root of you project, do dune describe pp ./path/to/the/file.ml. I don’t know if it works when there’s a typing issue like you have.

If it doesn’t work, you can do something like _build/default/.ppx/SOMEHASH/ppx.exe path/to/your/file.ml.

When you are just experimenting you can get it from utop, by launching it with the -dsource flag

$ utop -dsource

# #require  "lwt.unix";;
# let () = Lwt_main.run (let%lwt () = Lwt.return () in Lwt.return ());;

let () =
  Lwt_main.run
    (let __ppx_lwt_0 = Lwt.return () in
     let module Reraise = struct external reraise : exn -> 'a = "%reraise" end
       in
       Lwt.backtrace_bind
         (fun exn -> try Reraise.reraise exn with | exn -> exn) __ppx_lwt_0
         (fun () -> Lwt.return ()));;
4 Likes

I buy this logic. So the most direct way is probably to rewrite the ppx? I.e. figure out what functions its using whose signature changed from stdlib to Core, and patch those ?

I.e. I probably just need to monkey patch jsoo-react/ppx at main · ml-in-barcelona/jsoo-react · GitHub ?

../_build/default/.ppx/
0224ad3443a846e54f1637fccb074e7d/ 7b799aed44581cc79b02033532c5f775/ 8b4b27ee2f7fa2203a4d4f627658ee35/ a8555061cdeddb7e953f315191f569ac/ c3461e88ae1503d575d582eb5874e916/
28bc6bc83112f9cb7c72fe6e0d6fdfc4/ 878efc8ece20bcbf2dd8c437a0746ae0/ 8eae8b598e4d481b915e0cd9bfb82e6e/ b346392829ee81a810036ec11e96ae89/ cb0c9717da45983008c958dd4415bd16/

How do I pick which one? Does it matter?

Thank you for sharing this debugging tip. This is very useful.

min failure case:

open Core
module H = React.Dom.Dsl.Html

module Window = struct
  let t = H.div [||] []
  let%component make () = t
end

error:

File "ml/widget/min.ml", line 6, characters 2-27:
6 |   let%component make () = t
      ^^^^^^^^^^^^^^^^^^^^^^^^^
Error (warning 6 [labels-omitted]): label f was omitted in the application of this function.
File "ml/widget/min.ml", line 6, characters 2-27:
6 |   let%component make () = t
      ^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type
         string -> Js_of_ocaml.Js.js_string Js_of_ocaml.Js.t
       but an expression was expected of type 'a option

expanded output:

[@@@ocaml.ppx.context
  {
    tool_name = "ppx_driver";
    include_dirs = [];
    load_path = [];
    open_modules = [];
    for_package = None;
    debug = false;
    use_threads = false;
    use_vmthreads = false;
    recursive_types = false;
    principal = false;
    transparent_modules = false;
    unboxed_types = false;
    unsafe_string = false;
    cookies =
      [("library-name", "widget");
      ("ppx_optcomp.env",
        (env ~flambda2:(Defined false) ~flambda_backend:(Defined false)
           ~ocaml_version:(Defined (5, 0, 0)) ~os_type:(Defined "Unix")))]
  }]
let () =
  Ppx_module_timer_runtime.record_start Ppx_module_timer_runtime.__MODULE__
let () = Ppx_bench_lib.Benchmark_accumulator.Current_libname.set "widget"
let () =
  Expect_test_collector.Current_file.set
    ~absolute_filename:"ml/widget/min.ml"
let () = Ppx_inline_test_lib.Runtime.set_lib_and_partition "widget" ""
open Core
module H = React.Dom.Dsl.Html
module Window =
  struct
    let t = H.div [||] []
    let make =
      let make_props : ?key:string -> unit -> <  >  Js_of_ocaml.Js.t =
        fun ?key ->
          fun () ->
            let open Js_of_ocaml.Js.Unsafe in
              obj
                [|("key",
                    (inject
                       (Js_of_ocaml.Js.Optdef.option
                          (Option.map Js_of_ocaml.Js.string key))))|]
        [@@merlin.hide ] in
      let make () = (t : React.element) in
      ((let make (Props : <  >  Js_of_ocaml.Js.t) = make () in
        fun ?key -> fun () -> React.create_element make (make_props ?key ()))
        [@merlin.hide ])
  end
let () = Ppx_inline_test_lib.Runtime.unset_lib "widget"
let () = Expect_test_collector.Current_file.unset ()
let () = Ppx_bench_lib.Benchmark_accumulator.Current_libname.unset ()
let () =
  Ppx_module_timer_runtime.record_until Ppx_module_timer_runtime.__MODULE__
1 Like

Is the issue that Option.map got its arguments switched in going from stdlib to janestreet core ?

stdlib: OCaml library : Stdlib.Option

val map : ('a -> 'b) -> 'a option -> 'b option

janestreet core: Option (base.Base.Option)

val map : 'a t -> f:('a -> 'b) -> 'b t

Thank you @beajeanm for identifying the bug.

Thank you @Khady for the debugging trick. This is a life changer for debugging ppx.

For anyone finding this via google: it was jut a 3 line fix to https://github.com/ml-in-barcelona/jsoo-react/tree/main/ppx , swapping the args on Option.map in 3 places and adding ~f:

2 Likes

For what it’s worth…you could also do something like this if you didn’t want to change the ppx:

module Window = struct
  module Option = struct
    include Option

    (* Use the stdlib [map] function in this custom [Option] module. *)
    let map = Caml.Option.map
  end

  let%component make () = failwith ""
end

A better fix would be to qualify stdlib path used by the ppx expansion. For example, use Stdlib.Option.map instead of Option.map

Yes came here to say this. Instead of forking or patching the lib, It would be nice to open a PR with the change directly.

And please, open the issue related to jsoo-react in the repo (I’m fine if you open the issue in two places) but it might happen that we won’t see this post.

Here’s the PR Prefix Option and Array with Stdlib by davesnx · Pull Request #178 · ml-in-barcelona/jsoo-react · GitHub if you want to pin it.

3 Likes

I just noticed: your commit fixes two cases I missed: Array.map and Array.of_list

it does matter, but I don’t know how to pick the right one. If I end up needing this I usually try a bunch of them until I find the one which is taking care of the annotation I have trouble with.

You figured it out already. But for people who wonders how to get there, you can simply replace your original code with the expanded version and the compiler will point you to the exact line that is creating the issue.