Skip to content

Commit c72928e

Browse files
authoredMay 23, 2023
Add %ffi as variation of %raw with arity checking. (#6251)
* Begin exposing %raw arity Only print to stderr for now? See #6213 * Use %ffi attribute and handle arity zero Move to using `%ffi` extension to avoid breaking changes, and begin checking arity zero. For a JS function of arity 0, check that the ReScript type is `unit => _`. * Check all arities and make error message gpt3.5-proof. Extend arity check to all arities. Iterate on the error message so gpt3.5 can figure out a correct fix when given the error message with no context. * One more example. * cleanup * format * comment * Update CHANGELOG.md
1 parent 86c905a commit c72928e

9 files changed

+87
-11
lines changed
 

‎CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
1313
# 11.0.0-beta.2 (Unreleased)
1414

15+
#### :rocket: New Feature
16+
17+
- Introduced a new `%ffi` extension that provides a more robust mechanism for JavaScript function interoperation by considering function arity in type constraints. This enhancement improves safety when dealing with JavaScript functions by enforcing type constraints based on the arity of the function. [PR #6251](https://github.com/rescript-lang/rescript-compiler/pull/6251)
18+
1519
# 11.0.0-beta.1
1620

1721
#### :rocket: Main New Feature

‎jscomp/frontend/ast_exp_extension.ml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ open Ast_helper
2626
let handle_extension e (self : Bs_ast_mapper.mapper)
2727
(({txt; loc}, payload) : Parsetree.extension) =
2828
match txt with
29+
| "ffi" -> Ast_exp_handle_external.handle_ffi ~loc ~payload
2930
| "bs.raw" | "raw" ->
3031
Ast_exp_handle_external.handle_raw ~kind:Raw_exp loc payload
3132
| "bs.re" | "re" ->

‎jscomp/frontend/ast_exp_handle_external.ml

+50-4
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ let handle_debugger loc (payload : Ast_payload.t) =
7272
Location.raise_errorf ~loc "%%debugger extension doesn't accept arguments"
7373

7474
let handle_raw ~kind loc payload =
75-
let is_function = ref false in
75+
let is_function = ref None in
7676
match Ast_payload.raw_as_string_exp_exn ~kind ~is_function payload with
7777
| None -> (
7878
match kind with
@@ -93,11 +93,57 @@ let handle_raw ~kind loc payload =
9393
~pval_type:(Typ.arrow Nolabel (Typ.any ()) (Typ.any ()))
9494
[exp];
9595
pexp_attributes =
96-
(if !is_function then
97-
Ast_attributes.internal_expansive :: exp.pexp_attributes
98-
else exp.pexp_attributes);
96+
(match !is_function with
97+
| None -> exp.pexp_attributes
98+
| Some _ -> Ast_attributes.internal_expansive :: exp.pexp_attributes);
9999
}
100100

101+
let handle_ffi ~loc ~payload =
102+
let is_function = ref None in
103+
let err () =
104+
Location.raise_errorf ~loc
105+
"%%ffi extension can only be applied to a string containing a JavaScript \
106+
function such as \"(x) => ...\""
107+
in
108+
match
109+
Ast_payload.raw_as_string_exp_exn ~kind:Raw_exp ~is_function payload
110+
with
111+
| None -> err ()
112+
| Some exp ->
113+
(* Wrap a type constraint based on arity.
114+
E.g. for arity 2 constrain to type (_, _) => _ *)
115+
let wrapTypeConstraint (e : Parsetree.expression) =
116+
let loc = e.pexp_loc in
117+
let any = Ast_helper.Typ.any ~loc:e.pexp_loc () in
118+
let unit = Ast_literal.type_unit ~loc () in
119+
let rec arrow ~arity =
120+
if arity = 0 then Ast_helper.Typ.arrow ~loc Nolabel unit any
121+
else if arity = 1 then Ast_helper.Typ.arrow ~loc Nolabel any any
122+
else Ast_helper.Typ.arrow ~loc Nolabel any (arrow ~arity:(arity - 1))
123+
in
124+
match !is_function with
125+
| Some arity ->
126+
let type_ =
127+
Ast_uncurried.uncurriedType ~loc
128+
~arity:(if arity = 0 then 1 else arity)
129+
(arrow ~arity)
130+
in
131+
Ast_helper.Exp.constraint_ ~loc e type_
132+
| _ -> err ()
133+
in
134+
wrapTypeConstraint
135+
{
136+
exp with
137+
pexp_desc =
138+
Ast_external_mk.local_external_apply loc ~pval_prim:["#raw_expr"]
139+
~pval_type:(Typ.arrow Nolabel (Typ.any ()) (Typ.any ()))
140+
[exp];
141+
pexp_attributes =
142+
(match !is_function with
143+
| None -> exp.pexp_attributes
144+
| Some _ -> Ast_attributes.internal_expansive :: exp.pexp_attributes);
145+
}
146+
101147
let handle_raw_structure loc payload =
102148
match Ast_payload.raw_as_string_exp_exn ~kind:Raw_program payload with
103149
| Some exp ->

‎jscomp/frontend/ast_exp_handle_external.mli

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ val handle_external : Location.t -> string -> Parsetree.expression
2626

2727
val handle_debugger : Location.t -> Ast_payload.t -> Parsetree.expression_desc
2828

29+
val handle_ffi : loc:Location.t -> payload:Ast_payload.t -> Parsetree.expression
30+
2931
val handle_raw :
3032
kind:Js_raw_info.raw_kind ->
3133
Location.t ->

‎jscomp/ml/ast_payload.ml

+5-5
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,11 @@ let raw_as_string_exp_exn ~(kind : Js_raw_info.raw_kind) ?is_function (x : t) :
138138
Location.raise_errorf ~loc
139139
"Syntax error: a valid JS regex literal expected");
140140
(match is_function with
141-
| Some is_function -> (
142-
match Classify_function.classify_exp prog with
143-
| Js_function {arity = _; _} -> is_function := true
144-
| _ -> ())
145-
| None -> ());
141+
| Some is_function -> (
142+
match Classify_function.classify_exp prog with
143+
| Js_function {arity; _} -> is_function := Some arity
144+
| _ -> ())
145+
| None -> ());
146146
errors
147147
| Raw_program -> snd (Parser_flow.parse_program false None str));
148148
Some {e with pexp_desc = Pexp_constant (Pconst_string (str, None))}

‎jscomp/ml/ast_payload.mli

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ val is_single_ident : t -> Longident.t option
4747

4848
val raw_as_string_exp_exn :
4949
kind:Js_raw_info.raw_kind ->
50-
?is_function:bool ref ->
50+
?is_function:int option ref ->
5151
t ->
5252
Parsetree.expression option
5353
(** Convert %raw into expression *)

‎jscomp/test/FFI.js

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎jscomp/test/FFI.res

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@@uncurried
2+
3+
let canUseCanvas: unit => bool = %ffi(`
4+
function canUseCanvas() {
5+
return !!document.createElement('canvas').getContext;
6+
}
7+
`)
8+
9+
let add: (int, int) => int = %ffi(`(x,y)=>x+y`)

‎jscomp/test/build.ninja

+2-1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.