Skip to content

@notUndefined attribute for abstract types #7458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Add bitwise NOT (`~`) operator for `int` and `bigint`. https://github.com/rescript-lang/rescript/pull/7418
- 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
- JSX: do not error on ref as prop anymore (which is allowed in React 19). https://github.com/rescript-lang/rescript/pull/7420
- 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

#### :bug: Bug fix

Expand All @@ -42,7 +43,6 @@
#### :nail_care: Polish

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

# 12.0.0-alpha.12
Expand Down
13 changes: 13 additions & 0 deletions analysis/src/CompletionDecorators.ml
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,17 @@ let toplevel =

[Read more and see examples in the documentation](https://rescript-lang.org/docs/manual/latest/jsx#file-level-configuration).|};
] );
( "notUndefined",
None,
[
{|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`.

Example usage:
```rescript
@notUndefined
type t
```

[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#notundefined-decorator).|};
] );
]
21 changes: 21 additions & 0 deletions compiler/ml/typedecl.ml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type error =
| Unbound_type_var_ext of type_expr * extension_constructor
| Varying_anonymous
| Val_in_structure
| Invalid_attribute of string
| Bad_immediate_attribute
| Bad_unboxed_attribute of string
| Boxed_and_unboxed
Expand Down Expand Up @@ -288,13 +289,32 @@ let make_constructor env type_path type_params sargs sret_type =
widen z;
(targs, Some tret_type, args, Some ret_type, params)

let is_not_undefined_attr (attr : attribute) =
match attr with
| {Location.txt = "notUndefined"; _}, _ -> true
| _ -> false

(* Check that all the variables found in [ty] are in [univ].
Because [ty] is the argument to an abstract type, the representation
of that abstract type could be any subexpression of [ty], in particular
any type variable present in [ty].
*)

let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
(* Check for @notUndefined attribute *)
let has_not_undefined =
List.exists is_not_undefined_attr sdecl.ptype_attributes
in
(if has_not_undefined then
match (sdecl.ptype_kind, sdecl.ptype_manifest) with
| Ptype_abstract, None -> ()
| _ ->
raise
(Error
( sdecl.ptype_loc,
Invalid_attribute
"@notUndefined can only be used on abstract types" )));

(* Bind type parameters *)
reset_type_variables ();
Ctype.begin_def ();
Expand Down Expand Up @@ -2090,6 +2110,7 @@ let report_error ppf = function
"The field @{<info>%s@} is defined several times in this record. Fields \
can only be added once to a record."
s
| Invalid_attribute msg -> fprintf ppf "%s" msg
| Duplicate_label (s, Some record_name) ->
fprintf ppf
"The field @{<info>%s@} is defined several times in the record \
Expand Down
1 change: 1 addition & 0 deletions compiler/ml/typedecl.mli
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ val is_fixed_type : Parsetree.type_declaration -> bool

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

type native_repr_kind = Unboxed | Untagged

Expand Down
20 changes: 9 additions & 11 deletions compiler/ml/typeopt.ml
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,12 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) =
| For_sure_yes -> true
| For_sure_no -> false
| NA -> (
let untagged = ref false in
match
let decl = Env.find_type p env in
let () =
if Ast_untagged_variants.has_untagged decl.type_attributes then
untagged := true
in
decl.type_kind
with
let decl = Env.find_type p env in
match decl.type_kind with
| exception _ -> false
| Type_abstract | Type_open -> false
| Type_abstract ->
List.exists Typedecl.is_not_undefined_attr decl.type_attributes
| Type_open -> false
| Type_record _ -> true
| Type_variant
( [
Expand All @@ -74,10 +69,13 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) =
| [{cd_id = {name = "()"}; cd_args = Cstr_tuple []}] ) ->
false (* conservative *)
| Type_variant cdecls ->
let untagged =
Ast_untagged_variants.has_untagged decl.type_attributes
in
Ext_list.for_all cdecls (fun cd ->
if Ast_untagged_variants.has_undefined_literal cd.cd_attributes then
false
else if !untagged then
else if untagged then
match cd.cd_args with
| Cstr_tuple [t] ->
Ast_untagged_variants.type_is_builtin_object t
Expand Down
5 changes: 2 additions & 3 deletions runtime/Jsx.res
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */

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

@val external null: element = "null"

Expand Down
1 change: 1 addition & 0 deletions runtime/Stdlib_Date.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@notUndefined
type t

type msSinceEpoch = float
Expand Down
1 change: 1 addition & 0 deletions runtime/Stdlib_Date.resi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/**
A type representing a JavaScript date.
*/
@notUndefined
type t

/**
Expand Down
1 change: 1 addition & 0 deletions runtime/Stdlib_RegExp.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@notUndefined
type t

module Result = {
Expand Down
1 change: 1 addition & 0 deletions runtime/Stdlib_RegExp.resi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ See [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
/**
Type representing an instantiated `RegExp`.
*/
@notUndefined
type t

module Result: {
Expand Down
2 changes: 1 addition & 1 deletion tests/analysis_tests/tests/src/expected/Completion.res.txt

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions tests/build_tests/not_undefined_attribute/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check

import * as assert from "node:assert";
import { setup } from "#dev/process";
import { normalizeNewlines } from "#dev/utils";

const { execBuild } = setup(import.meta.dirname);

const out = await execBuild();

assert.equal(
normalizeNewlines(out.stdout.slice(out.stdout.indexOf("input.res:2:1-12"))),
`input.res:2:1-12

1 │ @notUndefined
2 │ type t = int
3 │

@notUndefined can only be used on abstract types

FAILED: cannot make progress due to previous errors.
`,
);
2 changes: 2 additions & 0 deletions tests/build_tests/not_undefined_attribute/input.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@notUndefined
type t = int
5 changes: 5 additions & 0 deletions tests/build_tests/not_undefined_attribute/rescript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "not_undefined_attribute",
"version": "0.1.0",
"sources": ["."]
}
60 changes: 60 additions & 0 deletions tests/tests/src/option_wrapping_test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Generated by ReScript, PLEASE EDIT WITH CARE

import * as Primitive_option from "rescript/lib/es6/Primitive_option.js";

let x6 = {
x: 42
};

let x7 = [
1,
2,
3
];

let x8 = () => {};

let x10 = null;

let x11 = Primitive_option.some(undefined);

let x20 = null;

let x21 = new Date();

let x22 = /test/;

let x1 = "hello";

let x2 = 1;

let x3 = {
TAG: "Ok",
_0: "hi"
};

let x4 = "polyvar";

let x5 = {
x: 42
};

let x12 = "test";

export {
x1,
x2,
x3,
x4,
x5,
x6,
x7,
x8,
x10,
x11,
x12,
x20,
x21,
x22,
}
/* x20 Not a pure module */
18 changes: 18 additions & 0 deletions tests/tests/src/option_wrapping_test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type test = {x: int}

let x1 = Some("hello")
let x2 = Some(1)
let x3 = Some(Ok("hi"))
let x4 = Some(#polyvar)
let x5 = Some({x: 42})
let x6 = Some({"x": 42})
let x7 = Some([1, 2, 3])
let x8 = Some(() => ())

let x10 = Some(Nullable.null)
let x11 = Some(Nullable.undefined)
let x12 = Some(Nullable.Value("test"))

let x20 = Some(Jsx.null)
let x21 = Some(Date.make())
let x22 = Some(/test/)
Loading