Skip to content

Commit c3f18d7

Browse files
committed
Fallback to Object.hasOwn
1 parent 635ea1c commit c3f18d7

17 files changed

+72
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
#### :rocket: New Feature
1616

1717
- Add `Dict.has` and double `Dict.forEachWithKey`/`Dict.mapValues` performance. https://github.com/rescript-lang/rescript/pull/7316
18+
- Make `Dict.has` inline `in` operator check and fallback to `Object.hasOwn` to guarantee safety. https://github.com/rescript-lang/rescript/pull/7342
1819
- Add popover attributes to JsxDOM.domProps. https://github.com/rescript-lang/rescript/pull/7317
1920
- Add `inert` attribute to `JsxDOM.domProps`. https://github.com/rescript-lang/rescript/pull/7326
2021
- Make reanalyze exception tracking work with the new stdlib. https://github.com/rescript-lang/rescript/pull/7328
2122
- Fix Pervasive.max using boolean comparison for floats. https://github.com/rescript-lang/rescript/pull/7333
22-
- Add built-in support for the JavaScript in operator. https://github.com/rescript-lang/rescript/pull/7342
2323

2424
#### :boom: Breaking Change
2525

compiler/core/js_exp_make.ml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,28 @@ let or_ ?comment (e1 : t) (e2 : t) =
11311131
let in_ (prop : t) (obj : t) : t =
11321132
{expression_desc = In (prop, obj); comment = None}
11331133

1134+
let has (obj : t) (prop : t) : t =
1135+
let non_prototype_prop =
1136+
match prop.expression_desc with
1137+
| Str
1138+
{
1139+
txt =
1140+
( "__proto__" | "toString" | "toLocaleString" | "valueOf"
1141+
| "hasOwnProperty" | "isPrototypeOf" | "propertyIsEnumerable" );
1142+
} ->
1143+
false
1144+
(* Optimize to use the in operator when property is a known string which is not a prototype property *)
1145+
| Str _ -> true
1146+
(* We can be sure in this case that the prop is not a prototype property like __proto__ or toString *)
1147+
| _ -> false
1148+
in
1149+
if non_prototype_prop then in_ prop obj
1150+
else
1151+
call
1152+
~info:{arity = Full; call_info = Call_na}
1153+
(js_global "Object.hasOwn")
1154+
[obj; prop]
1155+
11341156
let not (e : t) : t =
11351157
match e.expression_desc with
11361158
| Number (Int {i; _}) -> bool (i = 0l)

compiler/core/js_exp_make.mli

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ val or_ : ?comment:string -> t -> t -> t
354354

355355
val in_ : t -> t -> t
356356

357+
val has : t -> t -> t
358+
357359
(** we don't expose a general interface, since a general interface is generally not safe *)
358360

359361
val dummy_obj : ?comment:string -> Lam_tag_info.t -> t

compiler/core/lam_analysis.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ let rec no_side_effects (lam : Lam.t) : bool =
7777
(* list primitives *)
7878
| Pmakelist
7979
(* dict primitives *)
80-
| Pmakedict | Phasin
80+
| Pmakedict | Phas
8181
(* Test if the argument is a block or an immediate integer *)
8282
| Pisint | Pis_poly_var_block
8383
(* Test if the (integer) argument is outside an interval *)

compiler/core/lam_compile_primitive.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,12 +566,12 @@ let translate output_prefix loc (cxt : Lam_compile_context.t)
566566
Some (Js_op.Lit txt, expr)
567567
| _ -> None))
568568
| _ -> assert false)
569-
| Phasin -> (
569+
| Phas -> (
570570
match args with
571-
| [obj; prop] -> E.in_ prop obj
571+
| [obj; prop] -> E.has obj prop
572572
| _ ->
573573
Location.raise_errorf ~loc
574-
"Invalid external \"%%has_in\" type signature. Expected to have two \
574+
"Invalid external \"%%has\" type signature. Expected to have two \
575575
arguments.")
576576
| Parraysetu -> (
577577
match args with

compiler/core/lam_convert.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ let lam_prim ~primitive:(p : Lambda.primitive) ~args loc : Lam.t =
312312
| Parraysets -> prim ~primitive:Parraysets ~args loc
313313
| Pmakelist _mutable_flag (*FIXME*) -> prim ~primitive:Pmakelist ~args loc
314314
| Pmakedict -> prim ~primitive:Pmakedict ~args loc
315-
| Phasin -> prim ~primitive:Phasin ~args loc
315+
| Phas -> prim ~primitive:Phas ~args loc
316316
| Pawait -> prim ~primitive:Pawait ~args loc
317317
| Pimport -> prim ~primitive:Pimport ~args loc
318318
| Pinit_mod -> (

compiler/core/lam_primitive.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ type t =
135135
| Pmakelist
136136
(* dict primitives *)
137137
| Pmakedict
138-
| Phasin
138+
| Phas
139139
(* promise *)
140140
| Pawait
141141
(* etc or deprecated *)
@@ -214,7 +214,7 @@ let eq_primitive_approx (lhs : t) (rhs : t) =
214214
(* List primitives *)
215215
| Pmakelist
216216
(* dict primitives *)
217-
| Pmakedict | Phasin
217+
| Pmakedict | Phas
218218
(* promise *)
219219
| Pawait
220220
(* etc *)

compiler/core/lam_primitive.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ type t =
130130
| Pmakelist
131131
(* dict primitives *)
132132
| Pmakedict
133-
| Phasin
133+
| Phas
134134
(* promise *)
135135
| Pawait
136136
(* etc or deprecated *)

compiler/core/lam_print.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ let primitive ppf (prim : Lam_primitive.t) =
192192
| Pmakearray -> fprintf ppf "makearray"
193193
| Pmakelist -> fprintf ppf "makelist"
194194
| Pmakedict -> fprintf ppf "makedict"
195-
| Phasin -> fprintf ppf "has_in"
195+
| Phas -> fprintf ppf "has_in"
196196
| Parrayrefu -> fprintf ppf "array.unsafe_get"
197197
| Parraysetu -> fprintf ppf "array.unsafe_set"
198198
| Parrayrefs -> fprintf ppf "array.get"

compiler/ml/lambda.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ type primitive =
269269
| Pmakelist of Asttypes.mutable_flag
270270
(* dict primitives *)
271271
| Pmakedict
272-
| Phasin
272+
| Phas
273273
(* promise *)
274274
| Pawait
275275
(* module *)

compiler/ml/lambda.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ type primitive =
236236
| Pmakelist of Asttypes.mutable_flag
237237
(* dict primitives *)
238238
| Pmakedict
239-
| Phasin
239+
| Phas
240240
(* promise *)
241241
| Pawait
242242
(* modules *)

compiler/ml/printlambda.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ let primitive ppf = function
227227
| Pmakelist Mutable -> fprintf ppf "makelist"
228228
| Pmakelist Immutable -> fprintf ppf "makelist_imm"
229229
| Pmakedict -> fprintf ppf "makedict"
230-
| Phasin -> fprintf ppf "has_in"
230+
| Phas -> fprintf ppf "has_in"
231231
| Pisint -> fprintf ppf "isint"
232232
| Pisout -> fprintf ppf "isout"
233233
| Pisnullable -> fprintf ppf "isnullable"

compiler/ml/translcore.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ let primitives_table =
346346
("%array_unsafe_set", Parraysetu);
347347
(* dict primitives *)
348348
("%makedict", Pmakedict);
349-
("%has_in", Phasin);
349+
("%has", Phas);
350350
(* promise *)
351351
("%await", Pawait);
352352
(* module *)

runtime/Stdlib_Dict.res

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ let mapValues = (dict, f) => {
4141
target
4242
}
4343

44-
external has: (dict<'a>, string) => bool = "%has_in"
44+
external has: (dict<'a>, string) => bool = "%has"

runtime/Stdlib_Dict.resi

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,17 @@ let mapValues: (dict<'a>, 'a => 'b) => dict<'b>
246246
/**
247247
`has(dictionary, "key")` returns true if the "key" is present in the dictionary.
248248
249+
It uses the `in` operator under the hood and falls back to the `Object.hasOwn` method when safety cannot be guaranteed at compile time.
250+
249251
## Examples
250252
251253
```rescript
252254
let dict = dict{"key1": Some(1), "key2": None}
253255
254-
dict->Dict.has("key1") // true
255-
dict->Dict.has("key2") // true
256-
dict->Dict.has("key3") // false
257-
dict->Dict.has("__proto__") // true, since it uses in operator under the hood
256+
dict->Dict.has("key1")->assertEqual(true)
257+
dict->Dict.has("key2")->assertEqual(true)
258+
dict->Dict.has("key3")->assertEqual(false)
259+
dict->Dict.has("toString")->assertEqual(false)
258260
```
259261
*/
260-
external has: (dict<'a>, string) => bool = "%has_in"
262+
external has: (dict<'a>, string) => bool = "%has"

tests/tests/src/DictTests.mjs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ if (!("key1" in dict)) {
5151
RE_EXN_ID: "Assert_failure",
5252
_1: [
5353
"DictTests.res",
54-
43,
54+
44,
5555
2
5656
],
5757
Error: new Error()
@@ -63,7 +63,7 @@ if (!("key2" in dict)) {
6363
RE_EXN_ID: "Assert_failure",
6464
_1: [
6565
"DictTests.res",
66-
44,
66+
46,
6767
2
6868
],
6969
Error: new Error()
@@ -75,19 +75,31 @@ if ("key3" in dict !== false) {
7575
RE_EXN_ID: "Assert_failure",
7676
_1: [
7777
"DictTests.res",
78-
45,
78+
48,
7979
2
8080
],
8181
Error: new Error()
8282
};
8383
}
8484

85-
if (!("__proto__" in dict)) {
85+
if (Object.hasOwn(dict, "toString") !== false) {
8686
throw {
8787
RE_EXN_ID: "Assert_failure",
8888
_1: [
8989
"DictTests.res",
90-
46,
90+
50,
91+
2
92+
],
93+
Error: new Error()
94+
};
95+
}
96+
97+
if (!Object.hasOwn(dict, "key1")) {
98+
throw {
99+
RE_EXN_ID: "Assert_failure",
100+
_1: [
101+
"DictTests.res",
102+
52,
91103
2
92104
],
93105
Error: new Error()
@@ -99,7 +111,7 @@ if (typeof ("key1" in dict) !== "boolean") {
99111
RE_EXN_ID: "Assert_failure",
100112
_1: [
101113
"DictTests.res",
102-
47,
114+
54,
103115
2
104116
],
105117
Error: new Error()

tests/tests/src/DictTests.res

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ module DictHas = {
4040
"key2": None,
4141
}
4242

43+
// Test success path
4344
assert(dict->Dict.has("key1"))
45+
// Test undefined field
4446
assert(dict->Dict.has("key2"))
47+
// Test missing field
4548
assert(dict->Dict.has("key3") === false)
46-
assert(dict->Dict.has("__proto__"))
49+
// Test prototype field
50+
assert(dict->Dict.has("toString") === false)
51+
// Test without compile time knowledge
52+
assert(dict->Dict.has(%raw(`"key1"`)))
53+
// Test parantesis in generated code
4754
assert(typeof(dict->Dict.has("key1")) === #boolean)
4855
}

0 commit comments

Comments
 (0)