rescript-compiler icon indicating copy to clipboard operation
rescript-compiler copied to clipboard

When generating an interface file, externals with [@bs] attributes are incorrect

Open TheSpyder opened this issue 6 years ago • 11 comments

I have a module which includes externals with attributes such as bs.send and bs.module:

type t;
[@bs.scope "thing"] [@bs.module "jsmodule"] external fromJSON: Js.Dict.t('a) => t = "fromJSON";
[@bs.send] external toJSON: t => Js.Dict.t('a) = "toJSON";

However, when I generate an interface file (using either the vscode-reasonml project or bsc -i in 5.1.0) the file it produces is invalid:

type t;
[@bs...] external fromJSON: Js.Dict.t('a) => t = "fromJSON";
[@bs...] external toJSON: t => Js.Dict.t('a) = "toJSON";

Copying the source definitions to the interface file works.

A similar issue happens with generating OCaml interfaces, this syntax:

type t
external fromJSON : 'a Js.Dict.t -> t = "fromJSON"[@@bs.scope "thing"] [@@bs.module "jsmodule"]
external toJSON : t -> 'a Js.Dict.t = "toJSON"[@@bs.send]

produces the generated interface:

type t
external fromJSON : 'a Js.Dict.t -> t = "fromJSON" "BS-EXTERNAL"
external toJSON : t -> 'a Js.Dict.t = "toJSON" "BS-EXTERNAL"

Again copying the source to the interface resolves the issue.

TheSpyder avatar Sep 02 '19 15:09 TheSpyder

This is a known issue that we would like to fix eventually

bobzhang avatar Sep 12 '19 08:09 bobzhang

I didn't mention it above, but when I remove the invalid syntax I get errors like this:

       Values do not match:
         [@bs...] external type_: t => string = ""
       is not included in
         external type_: t => string = ""

I managed to create an interface file with let function signatures instead but now my JS has a function wrapper for every external definition:

function MyModule_000(prim) {
  return prim.type;
}

These simple functions shouldn't cause much runtime overhead, but the lack of externals on the interface makes the generated code using these externals a mess. It makes me want to delete the interface, which is really bad because it's full of unsafe things I want to hide.

TheSpyder avatar Sep 13 '19 15:09 TheSpyder

@TheSpyder I am a bit lost, I think you can always write external with bs annotations in the interface by hand, does not it work

bobzhang avatar Sep 16 '19 01:09 bobzhang

Oh I did say that, didn't I

Copying the source definitions to the interface file works.

I've had many occasions when copy/pasting doesn't work, but I looked a bit deeper and it turns out all of them were just very very confusing. For example cascading syntax errors resulting in a reason-vscode highlight on line 1 instead of at the correct location(s):

File "somefile.rei", line 15, characters 16-17:
Error: Syntax error: '}' expected
File "somefile.rei", line 4, characters 19-20:
Error: This '{' might be unmatched
File "somefile.rei", line 1:
Error: Error while running external preprocessor

I'm also dealing with a lot of complicated things:

  • local modules, where the reasonml difference between module X = {} and module X: {} is very subtle and easy to miss when copy/pasting, this is where the incorrect syntax error highlights tripped me up.
  • [@bs.deriving abstract] with [@bs.as "..."] renames - reason-vscode highlights the bs.as as unused in the interface file, and if I delete it the resulting error is entirely unhelpful:
       Values do not match:
         [@bs...] external object_: t => string = ""
       is not included in
         [@bs...] external object_: t => string = ""

I retract my statement above. Sorry. I can make this work.

TheSpyder avatar Sep 16 '19 08:09 TheSpyder

I had some additional confusion trying to use [@bs.deriving] on a record with normal use in the module but hiding the creation function in the interface, in the end it looks like adding pri on the interface while leaving it out in the implementation does work. I had some weird errors similar to above but it's all lining up now.

TheSpyder avatar Sep 16 '19 08:09 TheSpyder

This has become a lot worse in 7.2.2 (I'm not sure if it's specifically that version, I haven't tried to generate an interface for a while).

As an example, these externals:

[@bs.send] external empty: t => unit = "empty";

[@bs.send]
external isEmpty: (t, Js.Dict.t(bool), Js.Dict.t(bool), t => bool) => bool = "isEmpty";

Are generated to this mess in the interface: Screen Shot 2020-04-06 at 11 11 32 am

TheSpyder avatar Apr 06 '20 01:04 TheSpyder

@TheSpyder fixed in https://github.com/BuckleScript/bucklescript/pull/4284

bobzhang avatar Apr 07 '20 04:04 bobzhang

Awesome, thank you! I'll test it when the next release is available.

TheSpyder avatar Apr 09 '20 00:04 TheSpyder

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Mar 30 '22 03:03 stale[bot]

I guess I forgot to follow up two years ago. @bobzhang while it no longer prints binary data to the resi file, it's still wrong

sample implementation:

@scope("Something") @module("my-framework")
external assertHtml: (string, string, string) => unit = "assertHtml"

Generate interface creates:

external assertHtml: (string, string, string) => unit =
  "assertHtml" "#rescript-external"

TheSpyder avatar Mar 30 '22 09:03 TheSpyder