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

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

Open bobzhang opened this issue 6 years ago • 9 comments

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

bobzhang avatar Jun 20 '19 10:06 bobzhang

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)"

glennsl avatar Jun 20 '19 11:06 glennsl

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

bobzhang avatar Jun 20 '19 14:06 bobzhang

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.

glennsl avatar Jun 20 '19 17:06 glennsl

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

Risto-Stevcev avatar Sep 26 '19 08:09 Risto-Stevcev

@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?

jchavarri avatar May 05 '20 21:05 jchavarri

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 [email protected] 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 avatar May 06 '20 08:05 bobzhang

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)
}
|}

bobzhang avatar Jun 11 '20 03:06 bobzhang

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"

Risto-Stevcev avatar Jun 11 '20 09:06 Risto-Stevcev

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.

github-actions[bot] avatar Oct 17 '24 02:10 github-actions[bot]