rescript-lang / rescript

ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
https://rescript-lang.org
Other
6.76k stars 452 forks source link

Future work: Introducing a dsl for FFI instead of abusing attributes #3618

Closed bobzhang closed 1 month ago

bobzhang commented 5 years ago

basic

external%bs sum : int -> int -> int = "sum"
external%bs bark : string -> unit = "-> bark"
external%bs get : int array -> int -> int = " [ ] "
external%bs set : int array -> int -> int -> int = "  [ ] <- "

similar projects: https://github.com/fdopen/ppx_cstubs

glennsl commented 5 years ago

Fable does something similar with its Emit attribute, but uses templates with placeholders that mimic the actual JavaScript that will be generated instead of cryptic symbols that don't mean anything to anyone. See https://fable.io/docs/interacting.html#emit-attribute

Some examples of what this could look like:

external%bs sum : int -> int -> int = "sum($0, $1)"
let x = sum 1 2  (* Generates: var x = sum(1, 2) *)

(* Instead of [@bs.as {json|...|json}] *)
external%bs cloneNodeDeep : node -> node = "$0.cloneNode(true)"
external%bs attachShadowOpen : node -> node = "$0.attachShadow({ mode: 'open' })"

(* Generalized [@@bs.scope] *)
external%bs locationHref : document -> string = "$0.location.href"

(* Instead of [@@bs.set] *)
external%bs setTitle : document -> string -> unit = "$0.title = $1"

(* Instead of [@@bs.splice] / [@@bs.variadic] *)
external%bs add : collection -> string array -> unit = "$0.add($1...)"

(* Instead of [@@bs.new] *)
external%bs makeRegex : string -> regex = "new RegExp($0)"
bobzhang commented 5 years ago

Indeed, this looks more uniform, but it makes code analyzer more difficult. From what I can tell, it does not do any analysis or check though.

open Fable.Core
[<Emit("$0 - haha($1 + $2)")>]
let add (x: int) (y: string): float = jsNative
let result = add 1 "2"

generated code

export const result = 1 - haha("2" + null);

We could do some analysis, we will see how much effort it needs

glennsl commented 5 years ago

It does check both the JavaScript and template syntax (there's a bit more to it than just placeholders), but does not check the placeholders against the type signature it seems.

Fable is built on Babel which as I understand includes a JS parser, so that part is probably trivial for them to implement. I don't think it's necessary to support the entire JS syntax either, even just to cover the existing FFI surface. The above examples are pretty simple syntactically, especially if arguments are restricted to JSON, which of course you already have a parser for.

Hygienic macros with full support for the entirety of JavaScript's syntax would be pretty neat though.

Risto-Stevcev commented 5 years ago

Idris 1 does something similar too, it's really convenient

jchavarri commented 4 years ago

@bobzhang is this feature being considered in the current "official" roadmap? Or rather something that contributors could help with? This addition would make BuckleScript way more accessible and expressive for JavaScript developers. ❤️

A more specific question, there seems to be a Flow parser vendored already. (js_of_ocaml also has its own parser as well, based on Menhir).

Is the idea to use the vendored Flow parser for the JS snippets in these expressions?

bobzhang commented 4 years ago

yeah, it is the road map(after we finish the data representation changes). The flow parser is more tested (used more).

On Wed, May 6, 2020 at 5:21 AM Javier Chávarri notifications@github.com wrote:

@bobzhang https://github.com/bobzhang is this feature being considered in the current "official" roadmap? Or rather something that contributors could help with? This addition would make BuckleScript way more accessible and expressive for JavaScript developers. ❤️

A more specific question, there seems to be a Flow parser vendored already https://github.com/bucklescript/bucklescript/blob/7015ce12d629fd9b6385045e5b486f54a941d956/jscomp/js_parser/flow_ast.ml#L9-L12. (js_of_ocaml also has its own parser https://github.com/ocsigen/js_of_ocaml/blob/8263a4cb60bcb6944a71e73626b152fb4f3d1622/compiler/lib/js_parser.mly as well, based on Menhir).

Is the idea to use the vendored Flow parser for the JS snippets in these expressions?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/BuckleScript/bucklescript/issues/3618#issuecomment-624312911, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFWMK7P6K6SCNTS5API34TRQB7MNANCNFSM4HZRJTHQ .

-- Regards -- Hongbo Zhang

bobzhang commented 4 years ago

I am thinking we can just use the most intuitive way of writing FFIs

external add : int -> int -> int = "function(x,y){
   return x + y
}

Whether inline external or not is up to the compiler, for externals which are hard to inline, we can materialize it:

open struct 
   external add : ..
end 
: sig
   val add : int -> int -> int 
end

Compared with let x = %raw, we get polymorphism for free and can decide whether to inline it or not. Extra work to do: detect import from third party packages

external add : int -> int -> int = {|
var {fancyAdd} = require('third-party')
function(x,y){
   return fancyAdd(x,  y)
}
|}
Risto-Stevcev commented 4 years ago

My 2c,

I kind of like the existing Bucklescript FFI over this:

external add : int -> int -> int = {|
var {fancyAdd} = require('third-party')
function(x,y){
   return fancyAdd(x,  y)
}
|}

Purescript does something like this, and the FFI turned out to be a much bigger pain point than Bucklescript because of this kind of thing. When I switched to Bucklescript, writing bindings became much less of a burden. It takes longer to write bindings and it's easier to screw something up in the inlined js code, which ends up eating up time debugging things.

I originally thought the proposal here would be more Idris-like, sort of like this:

(* Keep bs.module so that bucklescript can output multiple module systems like commonjs
 * and es6 without it being hardcoded into the app code *)
external add : int -> int -> int = "fancyAdd($0, $1)" [@@bs.module "third-party"]
external add : int -> int -> int = "$0 + $1"

And from @glennsl 's comments, something like:

(* Generalized [@@bs.scope] *)
external locationHref : document -> string = "$0.location.href"

(* Instead of [@@bs.set] *)
external setTitle : document -> string -> unit = "$0.title = $1"
github-actions[bot] commented 1 month ago

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.