Skip to content

Commit ca03c17

Browse files
authored
@notundefined attribute for abstract types (#7458)
* @notundefined attribute for abstract types * Add @notundefined to the decorators file in the editor tooling * CHANGELOG * Error if @notundefined is used on a non-abstract type * Add build test for @notundefined error * Remove obsolete comment * InvalidAttribute -> Invalid_attribute * Extract function is_not_undefined_attr * Update analysis test output
1 parent d825723 commit ca03c17

File tree

16 files changed

+160
-16
lines changed

16 files changed

+160
-16
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- Add bitwise NOT (`~`) operator for `int` and `bigint`. https://github.com/rescript-lang/rescript/pull/7418
2525
- Significantly reduced the download size by splitting binaries into optional platform-specific dependencies (e.g, `@rescript/linux-x64`). https://github.com/rescript-lang/rescript/pull/7395
2626
- JSX: do not error on ref as prop anymore (which is allowed in React 19). https://github.com/rescript-lang/rescript/pull/7420
27+
- Add new attribute `@notUndefined` for abstract types to prevent unnecessary wrapping with `Primitive_option.some` in JS output. https://github.com/rescript-lang/rescript/pull/7458
2728

2829
#### :bug: Bug fix
2930

@@ -42,7 +43,6 @@
4243
#### :nail_care: Polish
4344

4445
- In type errors, recommend stdlib over Belt functions for converting between float/int/string. https://github.com/rescript-lang/rescript/pull/7453
45-
- Make `Jsx.element` a private empty record to avoid unnecessary `Primitive_option.some`. https://github.com/rescript-lang/rescript/pull/7450
4646
- Remove unused type `Jsx.ref`. https://github.com/rescript-lang/rescript/pull/7459
4747

4848
# 12.0.0-alpha.12

analysis/src/CompletionDecorators.ml

+13
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,17 @@ let toplevel =
297297

298298
[Read more and see examples in the documentation](https://rescript-lang.org/docs/manual/latest/jsx#file-level-configuration).|};
299299
] );
300+
( "notUndefined",
301+
None,
302+
[
303+
{|The `@notUndefined` decorator marks an abstract type as one that can never be `undefined` in JavaScript. This allows the compiler to generate more efficient code when the type is used inside an `option`.
304+
305+
Example usage:
306+
```rescript
307+
@notUndefined
308+
type t
309+
```
310+
311+
[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#notundefined-decorator).|};
312+
] );
300313
]

compiler/ml/typedecl.ml

+21
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type error =
4949
| Unbound_type_var_ext of type_expr * extension_constructor
5050
| Varying_anonymous
5151
| Val_in_structure
52+
| Invalid_attribute of string
5253
| Bad_immediate_attribute
5354
| Bad_unboxed_attribute of string
5455
| Boxed_and_unboxed
@@ -288,13 +289,32 @@ let make_constructor env type_path type_params sargs sret_type =
288289
widen z;
289290
(targs, Some tret_type, args, Some ret_type, params)
290291

292+
let is_not_undefined_attr (attr : attribute) =
293+
match attr with
294+
| {Location.txt = "notUndefined"; _}, _ -> true
295+
| _ -> false
296+
291297
(* Check that all the variables found in [ty] are in [univ].
292298
Because [ty] is the argument to an abstract type, the representation
293299
of that abstract type could be any subexpression of [ty], in particular
294300
any type variable present in [ty].
295301
*)
296302

297303
let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
304+
(* Check for @notUndefined attribute *)
305+
let has_not_undefined =
306+
List.exists is_not_undefined_attr sdecl.ptype_attributes
307+
in
308+
(if has_not_undefined then
309+
match (sdecl.ptype_kind, sdecl.ptype_manifest) with
310+
| Ptype_abstract, None -> ()
311+
| _ ->
312+
raise
313+
(Error
314+
( sdecl.ptype_loc,
315+
Invalid_attribute
316+
"@notUndefined can only be used on abstract types" )));
317+
298318
(* Bind type parameters *)
299319
reset_type_variables ();
300320
Ctype.begin_def ();
@@ -2090,6 +2110,7 @@ let report_error ppf = function
20902110
"The field @{<info>%s@} is defined several times in this record. Fields \
20912111
can only be added once to a record."
20922112
s
2113+
| Invalid_attribute msg -> fprintf ppf "%s" msg
20932114
| Duplicate_label (s, Some record_name) ->
20942115
fprintf ppf
20952116
"The field @{<info>%s@} is defined several times in the record \

compiler/ml/typedecl.mli

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ val is_fixed_type : Parsetree.type_declaration -> bool
6262

6363
(* for typeopt.ml *)
6464
val get_unboxed_type_representation : Env.t -> type_expr -> type_expr option
65+
val is_not_undefined_attr : Parsetree.attribute -> bool
6566

6667
type native_repr_kind = Unboxed | Untagged
6768

compiler/ml/typeopt.ml

+9-11
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,12 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) =
5050
| For_sure_yes -> true
5151
| For_sure_no -> false
5252
| NA -> (
53-
let untagged = ref false in
54-
match
55-
let decl = Env.find_type p env in
56-
let () =
57-
if Ast_untagged_variants.has_untagged decl.type_attributes then
58-
untagged := true
59-
in
60-
decl.type_kind
61-
with
53+
let decl = Env.find_type p env in
54+
match decl.type_kind with
6255
| exception _ -> false
63-
| Type_abstract | Type_open -> false
56+
| Type_abstract ->
57+
List.exists Typedecl.is_not_undefined_attr decl.type_attributes
58+
| Type_open -> false
6459
| Type_record _ -> true
6560
| Type_variant
6661
( [
@@ -74,10 +69,13 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) =
7469
| [{cd_id = {name = "()"}; cd_args = Cstr_tuple []}] ) ->
7570
false (* conservative *)
7671
| Type_variant cdecls ->
72+
let untagged =
73+
Ast_untagged_variants.has_untagged decl.type_attributes
74+
in
7775
Ext_list.for_all cdecls (fun cd ->
7876
if Ast_untagged_variants.has_undefined_literal cd.cd_attributes then
7977
false
80-
else if !untagged then
78+
else if untagged then
8179
match cd.cd_args with
8280
| Cstr_tuple [t] ->
8381
Ast_untagged_variants.type_is_builtin_object t

runtime/Jsx.res

+2-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@
2222
* along with this program; if not, write to the Free Software
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
2424

25-
// Define this as a private empty record so that the compiler does not
26-
// unnecessarily add `Primitive_option.some` calls for optional props.
27-
type element = private {}
25+
@notUndefined
26+
type element
2827

2928
@val external null: element = "null"
3029

runtime/Stdlib_Date.res

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@notUndefined
12
type t
23

34
type msSinceEpoch = float

runtime/Stdlib_Date.resi

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/**
66
A type representing a JavaScript date.
77
*/
8+
@notUndefined
89
type t
910

1011
/**

runtime/Stdlib_RegExp.res

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@notUndefined
12
type t
23

34
module Result = {

runtime/Stdlib_RegExp.resi

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ See [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
77
/**
88
Type representing an instantiated `RegExp`.
99
*/
10+
@notUndefined
1011
type t
1112

1213
module Result: {

tests/analysis_tests/tests/src/expected/Completion.res.txt

+1-1
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// @ts-check
2+
3+
import * as assert from "node:assert";
4+
import { setup } from "#dev/process";
5+
import { normalizeNewlines } from "#dev/utils";
6+
7+
const { execBuild } = setup(import.meta.dirname);
8+
9+
const out = await execBuild();
10+
11+
assert.equal(
12+
normalizeNewlines(out.stdout.slice(out.stdout.indexOf("input.res:2:1-12"))),
13+
`input.res:2:1-12
14+
15+
1 │ @notUndefined
16+
2 │ type t = int
17+
3 │
18+
19+
@notUndefined can only be used on abstract types
20+
21+
FAILED: cannot make progress due to previous errors.
22+
`,
23+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@notUndefined
2+
type t = int
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "not_undefined_attribute",
3+
"version": "0.1.0",
4+
"sources": ["."]
5+
}
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Generated by ReScript, PLEASE EDIT WITH CARE
2+
3+
import * as Primitive_option from "rescript/lib/es6/Primitive_option.js";
4+
5+
let x6 = {
6+
x: 42
7+
};
8+
9+
let x7 = [
10+
1,
11+
2,
12+
3
13+
];
14+
15+
let x8 = () => {};
16+
17+
let x10 = null;
18+
19+
let x11 = Primitive_option.some(undefined);
20+
21+
let x20 = null;
22+
23+
let x21 = new Date();
24+
25+
let x22 = /test/;
26+
27+
let x1 = "hello";
28+
29+
let x2 = 1;
30+
31+
let x3 = {
32+
TAG: "Ok",
33+
_0: "hi"
34+
};
35+
36+
let x4 = "polyvar";
37+
38+
let x5 = {
39+
x: 42
40+
};
41+
42+
let x12 = "test";
43+
44+
export {
45+
x1,
46+
x2,
47+
x3,
48+
x4,
49+
x5,
50+
x6,
51+
x7,
52+
x8,
53+
x10,
54+
x11,
55+
x12,
56+
x20,
57+
x21,
58+
x22,
59+
}
60+
/* x20 Not a pure module */
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type test = {x: int}
2+
3+
let x1 = Some("hello")
4+
let x2 = Some(1)
5+
let x3 = Some(Ok("hi"))
6+
let x4 = Some(#polyvar)
7+
let x5 = Some({x: 42})
8+
let x6 = Some({"x": 42})
9+
let x7 = Some([1, 2, 3])
10+
let x8 = Some(() => ())
11+
12+
let x10 = Some(Nullable.null)
13+
let x11 = Some(Nullable.undefined)
14+
let x12 = Some(Nullable.Value("test"))
15+
16+
let x20 = Some(Jsx.null)
17+
let x21 = Some(Date.make())
18+
let x22 = Some(/test/)

0 commit comments

Comments
 (0)