From ad4d423686e9ad634bc76881f8963086986c1300 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 17:25:28 +0000 Subject: [PATCH 01/11] Declare the runtime class or typeof of abstract types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Untagged variants recognize payloads of abstract types only from a hard-coded list of stdlib type names, which knows no subclasses and is closed to bindings such as WebAPI's (#7147). `@instanceOf("Class")` and `@typeOf("…")` on an abstract type or a type abbreviation state how its values are recognized at run time. The classifier reads them along the type's expansion chain: the outermost one decides, and the classes annotated further down a chain of private abbreviations are its superclasses. A class check excludes the subclasses that are cases too. Both imply that a value is never undefined. The name list stays as fallback for types without annotations. Editor completion offers both, and `@notUndefined`, which was listed with the file-level `@@` decorators, now completes after a single `@`. Representation added (untagged variant payload): Example: @instanceOf("Animal") type animal @instanceOf("Dog") type dog = private animal @typeOf("symbol") type sym @unboxed type t = Animal(animal) | Dog(dog) | Sym(sym) After (representation): Animal(a) ↦ a, recognized by a instanceof Animal && !(a instanceof Dog) Dog(d) ↦ d, recognized by d instanceof Dog Sym(s) ↦ s, recognized by typeof s === "symbol" Diagnostic added (invalid annotation): Example: @typeOf("undefined") type t After (diagnostic): @typeOf expects one of "string", "number", "bigint", "boolean", "symbol", "function" or "object", not "undefined". Review: Why `@instanceOf`/`@typeOf` rather than one `@runtime(...)` attribute? They read like the JavaScript checks they produce, take a single string like `@as` and `@tag`, and sit next to the existing `@notUndefined`. Review: Why also on public abbreviations? The typed array types are public aliases of `TypedArray.t<'a>` that users pass to `TypedArray` functions, so they can only be told apart by the name as written, as the name list does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel # Conflicts: # compiler/ext/config.ml --- analysis/src/completion_decorators.ml | 53 +++-- .../instanceof-typeof-annotations.feature.md | 1 + compiler/core/js_exp_make.ml | 16 +- compiler/core/lam_compile.ml | 9 +- compiler/ml/ast_untagged_variants.ml | 181 +++++++++++++++--- compiler/ml/lambda.ml | 2 +- compiler/ml/typedecl.ml | 3 + compiler/ml/typeopt.ml | 4 + compiler/ml/variant_layout.ml | 104 +++++++--- compiler/ml/variant_runtime.ml | 55 +----- compiler/ml/variant_runtime.mli | 35 ++-- .../tests/src/CompletionAttributes.res | 9 + .../src/expected/CompletionAttributes.res.txt | 59 ++++++ .../UntaggedAtMostOneInstance.res.expected | 2 +- .../instance_of_invalid_class.res.expected | 8 + .../expected/instance_of_keyword.res.expected | 8 + .../runtime_annotation_conflict.res.expected | 8 + .../runtime_annotation_on_record.res.expected | 8 + .../expected/type_of_invalid.res.expected | 8 + .../untagged_same_class_twice.res.expected | 10 + .../untagged_two_symbols.res.expected | 10 + .../fixtures/instance_of_invalid_class.res | 1 + .../fixtures/instance_of_keyword.res | 1 + .../fixtures/runtime_annotation_conflict.res | 1 + .../fixtures/runtime_annotation_on_record.res | 1 + .../super_errors/fixtures/type_of_invalid.res | 1 + .../fixtures/untagged_same_class_twice.res | 5 + .../fixtures/untagged_two_symbols.res | 5 + .../ounit_tests/ounit_string_literal_tests.ml | 4 +- ...tagged_runtime_annotations_shadow_test.mjs | 52 +++++ ...tagged_runtime_annotations_shadow_test.res | 44 +++++ .../src/untagged_runtime_annotations_test.mjs | 133 +++++++++++++ .../src/untagged_runtime_annotations_test.res | 142 ++++++++++++++ 33 files changed, 838 insertions(+), 145 deletions(-) create mode 100644 changes/instanceof-typeof-annotations.feature.md create mode 100644 tests/build_tests/super_errors/expected/instance_of_invalid_class.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_keyword.res.expected create mode 100644 tests/build_tests/super_errors/expected/runtime_annotation_conflict.res.expected create mode 100644 tests/build_tests/super_errors/expected/runtime_annotation_on_record.res.expected create mode 100644 tests/build_tests/super_errors/expected/type_of_invalid.res.expected create mode 100644 tests/build_tests/super_errors/expected/untagged_same_class_twice.res.expected create mode 100644 tests/build_tests/super_errors/expected/untagged_two_symbols.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_invalid_class.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_keyword.res create mode 100644 tests/build_tests/super_errors/fixtures/runtime_annotation_conflict.res create mode 100644 tests/build_tests/super_errors/fixtures/runtime_annotation_on_record.res create mode 100644 tests/build_tests/super_errors/fixtures/type_of_invalid.res create mode 100644 tests/build_tests/super_errors/fixtures/untagged_same_class_twice.res create mode 100644 tests/build_tests/super_errors/fixtures/untagged_two_symbols.res create mode 100644 tests/tests/src/untagged_runtime_annotations_shadow_test.mjs create mode 100644 tests/tests/src/untagged_runtime_annotations_shadow_test.res create mode 100644 tests/tests/src/untagged_runtime_annotations_test.mjs create mode 100644 tests/tests/src/untagged_runtime_annotations_test.res diff --git a/analysis/src/completion_decorators.ml b/analysis/src/completion_decorators.ml index 68d95385eb0..b5e5f1ccc3a 100644 --- a/analysis/src/completion_decorators.ml +++ b/analysis/src/completion_decorators.ml @@ -298,6 +298,46 @@ You will need this decorator whenever you want to use a JSX component in ReScrip [ {|The `@editor.completeFrom` decorator instructs the editor where it can draw additional completions from for this type.|}; ] ); + ( "instanceOf", + Some "instanceOf(\"$0\")", + [ + {|The `@instanceOf` decorator states that the values of an abstract type are instances of a global JavaScript class. Such a type can be the payload of an untagged variant case, which is then recognized with `instanceof`, and is never `undefined`, like with `@notUndefined`. + +A private abbreviation of another annotated type declares a subclass, so that the subclass case is checked first. + +Example usage: +```rescript +@instanceOf("Blob") +type blob + +@instanceOf("File") +type file = private blob +```|}; + ] ); + ( "typeOf", + Some "typeOf(\"$0\")", + [ + {|The `@typeOf` decorator states the JavaScript `typeof` of the values of an abstract type: one of `"string"`, `"number"`, `"bigint"`, `"boolean"`, `"symbol"`, `"function"` or `"object"`. Such a type can be the payload of an untagged variant case, and is never `undefined`, like with `@notUndefined`. + +Example usage: +```rescript +@typeOf("symbol") +type key +```|}; + ] ); + ( "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).|}; + ] ); ] let toplevel = @@ -331,17 +371,4 @@ 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).|}; - ] ); ] diff --git a/changes/instanceof-typeof-annotations.feature.md b/changes/instanceof-typeof-annotations.feature.md new file mode 100644 index 00000000000..bc1aea4ce24 --- /dev/null +++ b/changes/instanceof-typeof-annotations.feature.md @@ -0,0 +1 @@ +- Add `@instanceOf("Class")` and `@typeOf("…")` for abstract types and type abbreviations, so that values of bound JavaScript classes and opaque primitive types can be untagged variant cases. A private abbreviation of another annotated type declares a subclass. diff --git a/compiler/core/js_exp_make.ml b/compiler/core/js_exp_make.ml index 0f42b366495..e7bedaa582c 100644 --- a/compiler/core/js_exp_make.ml +++ b/compiler/core/js_exp_make.ml @@ -1385,14 +1385,25 @@ let literal_tag = function | Null -> nil | Undefined -> undefined +(* The constructor of a class at a global path such as "Intl.Locale": a + global with properties, so that a binding that shadows the global's name + is noticed. *) +let class_constructor (instance : Variant_runtime.Instance.t) = + match + String.split_on_char '.' (Variant_runtime.Instance.to_string instance) + with + | [] -> assert false + | global :: properties -> List.fold_left dot (js_global global) properties + (* The [typeof] string an untagged payload answers to. *) let block_type_name = function | Variant_runtime.IntType | FloatType -> str "number" | BigintType -> str "bigint" | BooleanType -> str "boolean" + | SymbolType -> str "symbol" | FunctionType -> str "function" | StringType -> str "string" - | InstanceType i -> js_global (Variant_runtime.Instance.to_string i) + | InstanceType i -> class_constructor i | ObjectType -> str "object" | UnknownType -> (* TODO: this should not happen *) @@ -1417,8 +1428,7 @@ let rec emit_check (check : t Ast_untagged_variants.Dynamic_checks.t) = | TypeOf x -> typeof (emit_check x) | IsInstanceOf (Array, x) -> is_array (emit_check x) | IsInstanceOf (instance, x) -> - let instance_name = Variant_runtime.Instance.to_string instance in - instanceof (emit_check x) (js_global instance_name) + instanceof (emit_check x) (class_constructor instance) | Not x -> not (emit_check x) | Expr x -> x diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 05fbf7762b3..ae2859dbd14 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -885,13 +885,20 @@ let compile output_prefix = let needs_array_guard = has_object_typeof && type_has_array_case && not clauses_have_array_case in + let instances = + Ext_list.filter_map block_cases (function + | Variant_runtime.InstanceType i -> Some i + | _ -> None) + in let rec build_if_chain remaining_clauses = match remaining_clauses with | ( Variant_runtime.Payload_shape (InstanceType instance_type), {J.switch_body} ) :: rest -> S.if_ - (E.emit_check (IsInstanceOf (instance_type, Expr e))) + (E.emit_check + (Ast_untagged_variants.Dynamic_checks.is_case_instance ~instances + instance_type (Expr e))) switch_body ~else_:[build_if_chain rest] | _ -> ( diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index e89c0660f5c..cc0029eb0a2 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -9,6 +9,7 @@ type untagged_error = | AtMostOneNumber | AtMostOneBigint | AtMostOneBoolean + | AtMostOneSymbol | UnreachableBoolean of string | DuplicateLiteral of string | SameLiteralValue of string * string @@ -20,6 +21,10 @@ type error = | InvalidVariantTagAnnotation | InvalidUntaggedVariantDefinition of untagged_error | TagFieldNameConflict of string * string * string + | InvalidInstanceOf + | InvalidTypeOf of string + | ConflictingRuntimeAnnotations + | RuntimeAnnotationOnRepresentedType of string exception Error of Location.t * error let report_error ppf = @@ -48,10 +53,11 @@ let report_error ppf = | AtMostOneInstance Array -> "At most one case can be an array or tuple type." | AtMostOneInstance i -> - "At most one case can be a " ^ Instance.to_string i ^ " type." + "At most one case can be an instance of " ^ Instance.to_string i ^ "." | AtMostOneFunction -> "At most one case can be a function type." | AtMostOneString -> "At most one case can be a string type." | AtMostOneBoolean -> "At most one case can be a boolean type." + | AtMostOneSymbol -> "At most one case can be a symbol type." | UnreachableBoolean name -> "Case " ^ name ^ " can never match: both true and false are already cases of this \ @@ -71,6 +77,22 @@ let report_error ppf = value of inline record field \"%s\". Use a different @tag name or \ rename the field." constructor_name runtime_value field_name + | InvalidInstanceOf -> + fprintf ppf + "@instanceOf expects the name of a global class, such as \ + @instanceOf(\"Date\") or @instanceOf(\"Intl.Locale\")." + | InvalidTypeOf name -> + fprintf ppf + "@typeOf expects one of \"string\", \"number\", \"bigint\", \"boolean\", \ + \"symbol\", \"function\" or \"object\"%s." + (if name = "" then "" else ", not \"" ^ name ^ "\"") + | ConflictingRuntimeAnnotations -> + fprintf ppf "A type can have either @instanceOf or @typeOf, not both." + | RuntimeAnnotationOnRepresentedType attribute -> + fprintf ppf + "@%s can only be used on abstract types and type abbreviations, whose \ + runtime representation it describes." + attribute (* Type of the runtime representation of a tag. @@ -145,6 +167,85 @@ let report_constructor_more_than_one_arg ~loc ~name = (Error (loc, InvalidUntaggedVariantDefinition (ConstructorMoreThanOneArg name))) +(* What [@instanceOf] or [@typeOf] on a type declaration states about the + runtime representation of its values. *) +type runtime_annotation = + | Instance_of of string (** instances of the global class at this path *) + | Type_of of block_type (** values with this [typeof] *) + +let instance_of = "instanceOf" +let type_of = "typeOf" + +let is_class_path name = + let is_start = function + | 'A' .. 'Z' | 'a' .. 'z' | '_' | '$' -> true + | _ -> false + in + let is_part = function + | '0' .. '9' -> true + | c -> is_start c + in + let is_identifier segment = + segment <> "" && is_start segment.[0] && String.for_all is_part segment + in + match String.split_on_char '.' name with + | global :: properties -> + (* Properties may be named like keywords, the global may not *) + is_identifier global + && (not (Js_reserved_map.is_js_keyword global)) + && List.for_all is_identifier properties + | [] -> false + +let block_type_of_typeof = function + | "string" -> Some StringType + | "number" -> Some FloatType + | "bigint" -> Some BigintType + | "boolean" -> Some BooleanType + | "symbol" -> Some SymbolType + | "function" -> Some FunctionType + | "object" -> Some ObjectType + | _ -> None + +(* The [@instanceOf] or [@typeOf] annotation in [attrs], validated. Only + [Typedecl] passes [~loc]; later readers see attributes it accepted. *) +let process_runtime_annotation ?(loc = Location.none) + (attrs : Parsetree.attributes) = + let annotation_of ({Location.txt; loc = attr_loc}, payload) = + let loc = if attr_loc = Location.none then loc else attr_loc in + if txt = instance_of then + match Ast_payload.semantic_string_of_payload payload with + | Some name when is_class_path name -> Some (Instance_of name) + | _ -> raise (Error (loc, InvalidInstanceOf)) + else if txt = type_of then + match Ast_payload.semantic_string_of_payload payload with + | Some name -> ( + match block_type_of_typeof name with + | Some block_type -> Some (Type_of block_type) + | None -> raise (Error (loc, InvalidTypeOf name))) + | None -> raise (Error (loc, InvalidTypeOf "")) + else None + in + match Ext_list.filter_map attrs annotation_of with + | [] -> None + | [annotation] -> Some annotation + | _ :: _ :: _ -> raise (Error (loc, ConflictingRuntimeAnnotations)) + +(* Checks the runtime annotations of a declaration whose kind is + [is_abstract_or_abbreviation]. *) +let check_runtime_annotation ~loc ~is_abstract_or_abbreviation + (attrs : Parsetree.attributes) = + match process_runtime_annotation ~loc attrs with + | None -> () + | Some annotation -> + if not is_abstract_or_abbreviation then + raise + (Error + ( loc, + RuntimeAnnotationOnRepresentedType + (match annotation with + | Instance_of _ -> instance_of + | Type_of _ -> type_of) )) + let type_is_builtin_object (t : Types.type_expr) = match t.desc with | Tconstr (Path.Pident ident, [_], _) when Ident.name ident = "dict" -> true @@ -154,33 +255,35 @@ let type_is_builtin_object (t : Types.type_expr) = | _ -> false let type_to_instanceof_backed_obj (t : Types.type_expr) = + let cls name = Some (Instance.Class {name; ancestors = []}) in match t.desc with | Tconstr (path, _, _) when Path.same path Predef.path_promise -> - Some Instance.Promise - | Tconstr (path, _, _) when Path.same path Predef.path_array -> Some Array + cls "Promise" + | Tconstr (path, _, _) when Path.same path Predef.path_array -> + Some Instance.Array | Tconstr (path, _, _) -> ( match Path.name path with - | "Stdlib_ArrayBuffer.t" -> Some ArrayBuffer - | "Stdlib.BigInt64Array.t" -> Some BigInt64Array - | "Stdlib.BigUint64Array.t" -> Some BigUint64Array - | "Stdlib.DataView.t" -> Some DataView - | "Stdlib_Date.t" -> Some Date - | "Stdlib.Float32Array.t" -> Some Float32Array - | "Stdlib.Float64Array.t" -> Some Float64Array - | "Stdlib.Int16Array.t" -> Some Int16Array - | "Stdlib.Int32Array.t" -> Some Int32Array - | "Stdlib.Int8Array.t" -> Some Int8Array - | "Stdlib_RegExp.t" -> Some RegExp - | "Stdlib.Uint16Array.t" -> Some Uint16Array - | "Stdlib.Uint32Array.t" -> Some Uint32Array - | "Stdlib.Uint8Array.t" -> Some Uint8Array - | "Stdlib.Uint8ClampedArray.t" -> Some Uint8ClampedArray - | "Stdlib_File.t" -> Some File - | "Stdlib_Blob.t" -> Some Blob - | "Stdlib.Set.t" -> Some Set - | "Stdlib.Map.t" -> Some Map - | "Stdlib.WeakSet.t" -> Some WeakSet - | "Stdlib.WeakMap.t" -> Some WeakMap + | "Stdlib_ArrayBuffer.t" -> cls "ArrayBuffer" + | "Stdlib.BigInt64Array.t" -> cls "BigInt64Array" + | "Stdlib.BigUint64Array.t" -> cls "BigUint64Array" + | "Stdlib.DataView.t" -> cls "DataView" + | "Stdlib_Date.t" -> cls "Date" + | "Stdlib.Float32Array.t" -> cls "Float32Array" + | "Stdlib.Float64Array.t" -> cls "Float64Array" + | "Stdlib.Int16Array.t" -> cls "Int16Array" + | "Stdlib.Int32Array.t" -> cls "Int32Array" + | "Stdlib.Int8Array.t" -> cls "Int8Array" + | "Stdlib_RegExp.t" -> cls "RegExp" + | "Stdlib.Uint16Array.t" -> cls "Uint16Array" + | "Stdlib.Uint32Array.t" -> cls "Uint32Array" + | "Stdlib.Uint8Array.t" -> cls "Uint8Array" + | "Stdlib.Uint8ClampedArray.t" -> cls "Uint8ClampedArray" + | "Stdlib_File.t" -> cls "File" + | "Stdlib_Blob.t" -> cls "Blob" + | "Stdlib.Set.t" -> cls "Set" + | "Stdlib.Map.t" -> cls "Map" + | "Stdlib.WeakSet.t" -> cls "WeakSet" + | "Stdlib.WeakMap.t" -> cls "WeakMap" | _ -> None) | _ -> None @@ -227,6 +330,7 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) let number_types = ref 0 in let bigint_types = ref 0 in let boolean_types = ref 0 in + let symbol_types = ref 0 in let unknown_types = ref 0 in let add_string_literal ~is_const ~loc s = let set = @@ -259,7 +363,7 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) if !object_types > 1 then raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneObject)); Hashtbl.iter - (fun i count -> + (fun _ (i, count) -> if count > 1 then raise (Error (loc, InvalidUntaggedVariantDefinition (AtMostOneInstance i)))) @@ -274,6 +378,8 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBigint)); if !boolean_types > 1 then raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBoolean)); + if !symbol_types > 1 then + raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneSymbol)); () in let check_literal ~is_const ~loc (tag : tag) = @@ -305,10 +411,15 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) | UnknownType -> incr unknown_types | ObjectType -> incr object_types | InstanceType i -> + (* A class is identified by its name *) + let name = Instance.to_string i in let count = - Hashtbl.find_opt instance_types i |> Option.value ~default:0 + match Hashtbl.find_opt instance_types name with + | Some (_, count) -> count + | None -> 0 in - Hashtbl.replace instance_types i (count + 1) + Hashtbl.replace instance_types name (i, count + 1) + | SymbolType -> incr symbol_types | FunctionType -> incr function_types | IntType | FloatType -> incr number_types | BigintType -> incr bigint_types @@ -402,6 +513,8 @@ module Dynamic_checks = struct let boolean = Payload_shape BooleanType |> tag_type + let symbol = Payload_shape SymbolType |> tag_type + let ( == ) x y = bin EqEqEq x y let ( != ) x y = bin NotEqEq x y let ( ||| ) x y = bin Or x y @@ -450,6 +563,7 @@ module Dynamic_checks = struct | BooleanType when literals_overlaps_with_boolean () = false -> typeof e != boolean | InstanceType i -> not (is_instance i e) + | SymbolType -> typeof e != symbol | FunctionType -> typeof e != function_ | ObjectType when literals_overlaps_with_object () = false -> typeof e != object_ @@ -517,6 +631,15 @@ module Dynamic_checks = struct else (* (undefiled + other) || other *) typeof e != object_ + (* An instance of the case's class [i] that is not an instance of a subclass + of [i] that is a case of its own: [instanceof] holds for superclasses + too. *) + let is_case_instance ~(instances : Instance.t list) i y = + Ext_list.fold_left instances (is_instance i y) (fun check other -> + if Instance.is_subclass other ~of_:i then + check &&& not (is_instance other y) + else check) + let add_runtime_type_check ~tag_type ~has_null_case ~(block_cases : block_type list) x y = let instances = @@ -527,7 +650,7 @@ module Dynamic_checks = struct match tag_type with | Payload_shape ( IntType | StringType | FloatType | BigintType | BooleanType - | FunctionType ) -> + | SymbolType | FunctionType ) -> typeof y == x | Payload_shape ObjectType -> let object_case = @@ -540,7 +663,7 @@ module Dynamic_checks = struct in not_one_of_the_instances else object_case - | Payload_shape (InstanceType i) -> is_instance i y + | Payload_shape (InstanceType i) -> is_case_instance ~instances i y | Payload_shape UnknownType -> (* This should not happen because unknown must be the only non-literal case *) assert false diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index b27e16e2410..bc3039e17b6 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -858,7 +858,7 @@ let value_has_block_type ~block_types (kind : value_kind) | UnknownType, (Is_literal _ | Is_object | Is_array) -> true | UnknownType, Unknown_value -> false | ( ( IntType | FloatType | StringType | BooleanType | ObjectType | BigintType - | FunctionType | InstanceType _ ), + | SymbolType | FunctionType | InstanceType _ ), _ ) -> false diff --git a/compiler/ml/typedecl.ml b/compiler/ml/typedecl.ml index 82a86327294..12d49c809b9 100644 --- a/compiler/ml/typedecl.ml +++ b/compiler/ml/typedecl.ml @@ -387,6 +387,9 @@ let transl_declaration ~type_record_as_object env sdecl id = ( sdecl.ptype_loc, Invalid_attribute "@notUndefined can only be used on abstract types" ))); + Ast_untagged_variants.check_runtime_annotation ~loc:sdecl.ptype_loc + ~is_abstract_or_abbreviation:(sdecl.ptype_kind = Ptype_abstract) + sdecl.ptype_attributes; (* Bind type parameters *) reset_type_variables (); diff --git a/compiler/ml/typeopt.ml b/compiler/ml/typeopt.ml index 0a4f1aa06de..83e63378bdf 100644 --- a/compiler/ml/typeopt.ml +++ b/compiler/ml/typeopt.ml @@ -38,6 +38,10 @@ let scrape env ty = (scrape_ty env ty).desc it will always be [Tvar] *) let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) = + (* A value declared to be a class instance or to have a [typeof] other than + "undefined" is not undefined *) + Option.is_some (Variant_layout.declared_block_type ~env typ) + || match scrape env typ with | Tconstr (p, _, _) -> ( (* all built in types could not inhabit none-like values: diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index f7cb3a3ad28..e57594c62b2 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -8,34 +8,86 @@ open Variant_runtime open Ast_untagged_variants +(* The [@instanceOf] and [@typeOf] annotations of [t] and of the types it + abbreviates, outermost first. The type as written can carry one, and so can + each level of a chain of private abbreviations that models a class + hierarchy, such as [@instanceOf("File") type t = private Blob.t]. *) +let runtime_annotations ~env (t : Types.type_expr) = + let rec collect ~previous (t : Types.type_expr) = + match (Btype.repr t).desc with + | Tconstr (path, _, _) -> + let here = + match Env.find_type path env with + | {type_attributes} -> process_runtime_annotation type_attributes + | exception Not_found -> None + in + (* A module alias makes [M.t] an abbreviation of the original [t] with + the same attributes, so an annotation repeating the one just seen is + the same declaration again *) + let here, previous = + match here with + | Some _ when here = previous -> (None, previous) + | Some _ -> (here, here) + | None -> (None, previous) + in + let rest = + match Ctype.try_expand_once_opt env t with + | expanded -> collect ~previous expanded + | exception Ctype.Cannot_expand -> [] + in + Option.to_list here @ rest + | _ -> [] + in + collect ~previous:None t + +(* The block type the outermost annotation of [t] states. A class's + ancestors are the classes annotated further down the chain. *) +let declared_block_type ~env t = + match runtime_annotations ~env t with + | Instance_of name :: rest -> + let ancestors = + List.filter_map + (function + | Instance_of ancestor when ancestor <> name -> Some ancestor + | Instance_of _ | Type_of _ -> None) + rest + in + Some (InstanceType (Class {name; ancestors})) + | Type_of block_type :: _ -> Some block_type + | [] -> None + let get_block_type_from_typ ~env (t : Types.type_expr) : block_type option = - (* First check the original (unexpanded) type for typed arrays and other instance types *) - match type_to_instanceof_backed_obj t with - | Some instance_type -> Some (InstanceType instance_type) + match declared_block_type ~env t with + | Some _ as declared -> declared | None -> ( - (* If original type didn't match, expand and try standard checks *) - let expanded_t = Ctype.expand_head env t in - match expanded_t with - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_string -> - Some StringType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_int -> - Some IntType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_float -> - Some FloatType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bigint -> - Some BigintType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bool -> - Some BooleanType - | {desc = Tarrow _} -> Some FunctionType - | {desc = Tconstr _} as expanded_t when type_is_builtin_object expanded_t -> - Some ObjectType - | {desc = Tconstr _} as expanded_t - when type_to_instanceof_backed_obj expanded_t |> Option.is_some -> ( - match type_to_instanceof_backed_obj expanded_t with - | None -> None - | Some instance_type -> Some (InstanceType instance_type)) - | {desc = Ttuple _} -> Some (InstanceType Array) - | _ -> None) + (* First check the original (unexpanded) type for typed arrays and other instance types *) + match type_to_instanceof_backed_obj t with + | Some instance_type -> Some (InstanceType instance_type) + | None -> ( + (* If original type didn't match, expand and try standard checks *) + let expanded_t = Ctype.expand_head env t in + match expanded_t with + | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_string -> + Some StringType + | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_int -> + Some IntType + | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_float -> + Some FloatType + | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bigint -> + Some BigintType + | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bool -> + Some BooleanType + | {desc = Tarrow _} -> Some FunctionType + | {desc = Tconstr _} as expanded_t when type_is_builtin_object expanded_t + -> + Some ObjectType + | {desc = Tconstr _} as expanded_t + when type_to_instanceof_backed_obj expanded_t |> Option.is_some -> ( + match type_to_instanceof_backed_obj expanded_t with + | None -> None + | Some instance_type -> Some (InstanceType instance_type)) + | {desc = Ttuple _} -> Some (InstanceType Array) + | _ -> None)) let get_block_type ~env (cstr : Types.constructor_declaration) : block_type = match cstr.cd_args with diff --git a/compiler/ml/variant_runtime.ml b/compiler/ml/variant_runtime.ml index e5db19f22ae..47696495d58 100644 --- a/compiler/ml/variant_runtime.ml +++ b/compiler/ml/variant_runtime.ml @@ -5,54 +5,16 @@ each declaration. *) module Instance = struct - type t = - | Array - | ArrayBuffer - | BigInt64Array - | BigUint64Array - | Blob - | DataView - | Date - | File - | Float32Array - | Float64Array - | Int16Array - | Int32Array - | Int8Array - | Promise - | RegExp - | Uint16Array - | Uint32Array - | Uint8Array - | Uint8ClampedArray - | Set - | Map - | WeakSet - | WeakMap + type t = Array | Class of {name: string; ancestors: string list} + let to_string = function | Array -> "Array" - | ArrayBuffer -> "ArrayBuffer" - | BigInt64Array -> "BigInt64Array" - | BigUint64Array -> "BigUint64Array" - | Blob -> "Blob" - | DataView -> "DataView" - | Date -> "Date" - | File -> "File" - | Float32Array -> "Float32Array" - | Float64Array -> "Float64Array" - | Int16Array -> "Int16Array" - | Int32Array -> "Int32Array" - | Int8Array -> "Int8Array" - | Promise -> "Promise" - | RegExp -> "RegExp" - | Uint16Array -> "Uint16Array" - | Uint32Array -> "Uint32Array" - | Uint8Array -> "Uint8Array" - | Uint8ClampedArray -> "Uint8ClampedArray" - | Set -> "Set" - | Map -> "Map" - | WeakSet -> "WeakSet" - | WeakMap -> "WeakMap" + | Class {name} -> name + + let is_subclass t ~of_ = + match (t, of_) with + | Class {ancestors}, Class {name} -> List.mem name ancestors + | (Array | Class _), _ -> false end (* Type of the runtime representation of an untagged block (case with payload) *) @@ -63,6 +25,7 @@ type block_type = | BigintType | BooleanType | InstanceType of Instance.t + | SymbolType | FunctionType | ObjectType | UnknownType diff --git a/compiler/ml/variant_runtime.mli b/compiler/ml/variant_runtime.mli index c8555da8c61..491a2fd3d35 100644 --- a/compiler/ml/variant_runtime.mli +++ b/compiler/ml/variant_runtime.mli @@ -1,30 +1,16 @@ module Instance : sig - type t = - | Array - | ArrayBuffer - | BigInt64Array - | BigUint64Array - | Blob - | DataView - | Date - | File - | Float32Array - | Float64Array - | Int16Array - | Int32Array - | Int8Array - | Promise - | RegExp - | Uint16Array - | Uint32Array - | Uint8Array - | Uint8ClampedArray - | Set - | Map - | WeakSet - | WeakMap + (** A JavaScript class whose instances a value is recognized by: + [name] is the global constructor's path, such as ["Date"] or + ["Intl.Locale"], and [ancestors] are the paths of its superclasses that + are declared too, nearest first. Arrays are recognized by + [Array.isArray] instead. *) + type t = Array | Class of {name: string; ancestors: string list} val to_string : t -> string + + val is_subclass : t -> of_:t -> bool + (** Whether every instance of the first is an instance of [of_] too, without + the two being the same class. *) end type block_type = @@ -34,6 +20,7 @@ type block_type = | BigintType | BooleanType | InstanceType of Instance.t + | SymbolType | FunctionType | ObjectType | UnknownType diff --git a/tests/analysis_tests/tests/src/CompletionAttributes.res b/tests/analysis_tests/tests/src/CompletionAttributes.res index 527cb64378e..97386f2d409 100644 --- a/tests/analysis_tests/tests/src/CompletionAttributes.res +++ b/tests/analysis_tests/tests/src/CompletionAttributes.res @@ -42,3 +42,12 @@ // type withTag = | @as("t") A | B // ^com + +// @instanc +// ^com + +// @typeO +// ^com + +// @notUnd +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt b/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt index 669d703af6a..2797cafdf06 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt @@ -315,3 +315,62 @@ Resolved opens 1 Stdlib } ] +Complete src/CompletionAttributes.res 45:11 +Attribute id:instanc:[45:3->45:11] label:instanc +Completable: Cdecorator(instanc) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +[ + { + "detail": "", + "documentation": { + "kind": "markdown", + "value": "The `@instanceOf` decorator states that the values of an abstract type are instances of a global JavaScript class. Such a type can be the payload of an untagged variant case, which is then recognized with `instanceof`, and is never `undefined`, like with `@notUndefined`.\n\nA private abbreviation of another annotated type declares a subclass, so that the subclass case is checked first.\n\nExample usage:\n```rescript\n@instanceOf(\"Blob\")\ntype blob\n\n@instanceOf(\"File\")\ntype file = private blob\n```" + }, + "insertText": "instanceOf(\"$0\")", + "insertTextFormat": 2, + "kind": 4, + "label": "instanceOf", + "tags": [] + } +] + +Complete src/CompletionAttributes.res 48:9 +Attribute id:typeO:[48:3->48:9] label:typeO +Completable: Cdecorator(typeO) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +[ + { + "detail": "", + "documentation": { + "kind": "markdown", + "value": "The `@typeOf` decorator states the JavaScript `typeof` of the values of an abstract type: one of `\"string\"`, `\"number\"`, `\"bigint\"`, `\"boolean\"`, `\"symbol\"`, `\"function\"` or `\"object\"`. Such a type can be the payload of an untagged variant case, and is never `undefined`, like with `@notUndefined`.\n\nExample usage:\n```rescript\n@typeOf(\"symbol\")\ntype key\n```" + }, + "insertText": "typeOf(\"$0\")", + "insertTextFormat": 2, + "kind": 4, + "label": "typeOf", + "tags": [] + } +] + +Complete src/CompletionAttributes.res 51:10 +Attribute id:notUnd:[51:3->51:10] label:notUnd +Completable: Cdecorator(notUnd) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +[ + { + "detail": "", + "documentation": { + "kind": "markdown", + "value": "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`.\n\nExample usage:\n```rescript\n@notUndefined\ntype t\n```\n\n[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#notundefined-decorator)." + }, + "insertTextFormat": 2, + "kind": 4, + "label": "notUndefined", + "tags": [] + } +] + diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected index 841902fcbdc..dd5e76e54ca 100644 --- a/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected @@ -6,4 +6,4 @@ 2 │ type t = A(Date.t) | B(Date.t) 3 │ - This untagged variant definition is invalid: At most one case can be a Date type. \ No newline at end of file + This untagged variant definition is invalid: At most one case can be an instance of Date. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_invalid_class.res.expected b/tests/build_tests/super_errors/expected/instance_of_invalid_class.res.expected new file mode 100644 index 00000000000..54f950d68b0 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_invalid_class.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_invalid_class.res:1:1-11 + + 1 │ @instanceOf("not a class") type t + 2 │ + + @instanceOf expects the name of a global class, such as @instanceOf("Date") or @instanceOf("Intl.Locale"). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_keyword.res.expected b/tests/build_tests/super_errors/expected/instance_of_keyword.res.expected new file mode 100644 index 00000000000..f70e95dcfd9 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_keyword.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_keyword.res:1:1-11 + + 1 │ @instanceOf("class") type t + 2 │ + + @instanceOf expects the name of a global class, such as @instanceOf("Date") or @instanceOf("Intl.Locale"). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/runtime_annotation_conflict.res.expected b/tests/build_tests/super_errors/expected/runtime_annotation_conflict.res.expected new file mode 100644 index 00000000000..1283d8bc21f --- /dev/null +++ b/tests/build_tests/super_errors/expected/runtime_annotation_conflict.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/runtime_annotation_conflict.res:1:39-44 + + 1 │ @instanceOf("Date") @typeOf("object") type t + 2 │ + + A type can have either @instanceOf or @typeOf, not both. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/runtime_annotation_on_record.res.expected b/tests/build_tests/super_errors/expected/runtime_annotation_on_record.res.expected new file mode 100644 index 00000000000..be3a6f008f0 --- /dev/null +++ b/tests/build_tests/super_errors/expected/runtime_annotation_on_record.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/runtime_annotation_on_record.res:1:21-37 + + 1 │ @instanceOf("Date") type t = {x: int} + 2 │ + + @instanceOf can only be used on abstract types and type abbreviations, whose runtime representation it describes. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/type_of_invalid.res.expected b/tests/build_tests/super_errors/expected/type_of_invalid.res.expected new file mode 100644 index 00000000000..f5ae2f7ad55 --- /dev/null +++ b/tests/build_tests/super_errors/expected/type_of_invalid.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/type_of_invalid.res:1:1-7 + + 1 │ @typeOf("undefined") type t + 2 │ + + @typeOf expects one of "string", "number", "bigint", "boolean", "symbol", "function" or "object", not "undefined". \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/untagged_same_class_twice.res.expected b/tests/build_tests/super_errors/expected/untagged_same_class_twice.res.expected new file mode 100644 index 00000000000..2a9f897db8e --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_same_class_twice.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/untagged_same_class_twice.res:5:25-34 + + 3 │ + 4 │ @unboxed + 5 │ type t = Animal(animal) | Pet(pet) + 6 │ + + This untagged variant definition is invalid: At most one case can be an instance of Animal. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/untagged_two_symbols.res.expected b/tests/build_tests/super_errors/expected/untagged_two_symbols.res.expected new file mode 100644 index 00000000000..1ffda84ce63 --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_two_symbols.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/untagged_two_symbols.res:5:15-20 + + 3 │ + 4 │ @unboxed + 5 │ type t = A(a) | B(b) + 6 │ + + This untagged variant definition is invalid: At most one case can be a symbol type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/instance_of_invalid_class.res b/tests/build_tests/super_errors/fixtures/instance_of_invalid_class.res new file mode 100644 index 00000000000..7e732e3d900 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_invalid_class.res @@ -0,0 +1 @@ +@instanceOf("not a class") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_keyword.res b/tests/build_tests/super_errors/fixtures/instance_of_keyword.res new file mode 100644 index 00000000000..8a02fcf5228 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_keyword.res @@ -0,0 +1 @@ +@instanceOf("class") type t diff --git a/tests/build_tests/super_errors/fixtures/runtime_annotation_conflict.res b/tests/build_tests/super_errors/fixtures/runtime_annotation_conflict.res new file mode 100644 index 00000000000..6830cf9096a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/runtime_annotation_conflict.res @@ -0,0 +1 @@ +@instanceOf("Date") @typeOf("object") type t diff --git a/tests/build_tests/super_errors/fixtures/runtime_annotation_on_record.res b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_record.res new file mode 100644 index 00000000000..0bc6dc82571 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_record.res @@ -0,0 +1 @@ +@instanceOf("Date") type t = {x: int} diff --git a/tests/build_tests/super_errors/fixtures/type_of_invalid.res b/tests/build_tests/super_errors/fixtures/type_of_invalid.res new file mode 100644 index 00000000000..2d4d3cbabb0 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/type_of_invalid.res @@ -0,0 +1 @@ +@typeOf("undefined") type t diff --git a/tests/build_tests/super_errors/fixtures/untagged_same_class_twice.res b/tests/build_tests/super_errors/fixtures/untagged_same_class_twice.res new file mode 100644 index 00000000000..b7dfd17ca9e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_same_class_twice.res @@ -0,0 +1,5 @@ +@instanceOf("Animal") type animal +@instanceOf("Animal") type pet + +@unboxed +type t = Animal(animal) | Pet(pet) diff --git a/tests/build_tests/super_errors/fixtures/untagged_two_symbols.res b/tests/build_tests/super_errors/fixtures/untagged_two_symbols.res new file mode 100644 index 00000000000..b36b376b4b8 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_two_symbols.res @@ -0,0 +1,5 @@ +@typeOf("symbol") type a +@typeOf("symbol") type b + +@unboxed +type t = A(a) | B(b) diff --git a/tests/ounit_tests/ounit_string_literal_tests.ml b/tests/ounit_tests/ounit_string_literal_tests.ml index a4a879cbd0d..f95904db709 100644 --- a/tests/ounit_tests/ounit_string_literal_tests.ml +++ b/tests/ounit_tests/ounit_string_literal_tests.ml @@ -775,7 +775,9 @@ let suites = OUnit.assert_failure "expected Array.isArray simplification to remain active"); let open Ast_untagged_variants.Dynamic_checks in - let date = Variant_runtime.Instance.Date in + let date = + Variant_runtime.Instance.Class {name = "Date"; ancestors = []} + in assert_js_global ~expected:"Date" (Js_exp_make.emit_check (TagType (Variant_runtime.Payload_shape (InstanceType date)))); diff --git a/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs b/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs new file mode 100644 index 00000000000..ce74261c117 --- /dev/null +++ b/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs @@ -0,0 +1,52 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +globalThis.UntaggedShadowed = class UntaggedShadowed {} +globalThis.UntaggedNs = {Klass: class Klass {}} +; + +let name = "local module"; + +let UntaggedShadowed = { + name: name +}; + +let name$1 = "local module"; + +let UntaggedNs = { + name: name$1 +}; + +function kind(value) { + if (value instanceof globalThis.UntaggedShadowed) { + return "shadowed"; + } + if (value instanceof globalThis.UntaggedNs.Klass) { + return "klass"; + } + switch (typeof value) { + case "string" : + return value; + } +} + +Mocha.describe("Untagged_runtime_annotations_shadow_test", () => { + Mocha.test("a class check reads a shadowed global", () => { + Test_utils.eq("File \"untagged_runtime_annotations_shadow_test.res\", line 35, characters 7-14", "local module", "local module"); + Test_utils.eq("File \"untagged_runtime_annotations_shadow_test.res\", line 36, characters 7-14", "shadowed", kind(new (globalThis.UntaggedShadowed)())); + }); + Mocha.test("a class check at a dotted path reads a shadowed global", () => { + Test_utils.eq("File \"untagged_runtime_annotations_shadow_test.res\", line 40, characters 7-14", "local module", "local module"); + Test_utils.eq("File \"untagged_runtime_annotations_shadow_test.res\", line 41, characters 7-14", "klass", kind(new (globalThis.UntaggedNs.Klass)())); + Test_utils.eq("File \"untagged_runtime_annotations_shadow_test.res\", line 42, characters 7-14", "text", "text"); + }); +}); + +export { + UntaggedShadowed, + UntaggedNs, + kind, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_runtime_annotations_shadow_test.res b/tests/tests/src/untagged_runtime_annotations_shadow_test.res new file mode 100644 index 00000000000..f993f408475 --- /dev/null +++ b/tests/tests/src/untagged_runtime_annotations_shadow_test.res @@ -0,0 +1,44 @@ +open Mocha +open Test_utils + +%%raw(` +globalThis.UntaggedShadowed = class UntaggedShadowed {} +globalThis.UntaggedNs = {Klass: class Klass {}} +`) + +// Bindings named like the globals the checks below refer to +module UntaggedShadowed = { + let name = "local module" +} +module UntaggedNs = { + let name = "local module" +} + +@instanceOf("UntaggedShadowed") type shadowed +@new external makeShadowed: unit => shadowed = "UntaggedShadowed" + +@instanceOf("UntaggedNs.Klass") type klass +@new @scope(("globalThis", "UntaggedNs")) external makeKlass: unit => klass = "Klass" + +@unboxed +type value = Shadowed(shadowed) | Klass(klass) | Text(string) + +let kind = value => + switch value { + | Shadowed(_) => "shadowed" + | Klass(_) => "klass" + | Text(text) => text + } + +describe(__MODULE__, () => { + test("a class check reads a shadowed global", () => { + eq(__LOC__, "local module", UntaggedShadowed.name ++ "") + eq(__LOC__, "shadowed", kind(Shadowed(makeShadowed()))) + }) + + test("a class check at a dotted path reads a shadowed global", () => { + eq(__LOC__, "local module", UntaggedNs.name ++ "") + eq(__LOC__, "klass", kind(Klass(makeKlass()))) + eq(__LOC__, "text", kind(Text("text"))) + }) +}) diff --git a/tests/tests/src/untagged_runtime_annotations_test.mjs b/tests/tests/src/untagged_runtime_annotations_test.mjs new file mode 100644 index 00000000000..e0f43c4c14a --- /dev/null +++ b/tests/tests/src/untagged_runtime_annotations_test.mjs @@ -0,0 +1,133 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +class Animal {} +class Dog extends Animal {} +globalThis.UntaggedAnimal = Animal +globalThis.UntaggedDog = Dog +; + +function describePet(pet) { + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } + if (pet instanceof UntaggedDog) { + return "dog"; + } + switch (typeof pet) { + case "string" : + return pet; + } +} + +function onlyAnimal(pet) { + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } + if (pet instanceof UntaggedDog) { + return "other"; + } + switch (typeof pet) { + case "string" : + return "other"; + } +} + +function describeReversed(pet) { + if (pet instanceof UntaggedDog) { + return "dog"; + } else { + return "animal"; + } +} + +let Zoo = {}; + +function describeAliased(pet) { + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } else { + return "dog"; + } +} + +function keyKind(key) { + if (typeof key === "symbol") { + return "symbol"; + } else { + return key; + } +} + +function referenceKind(reference) { + if (typeof reference === "string") { + return "id"; + } else { + return "index"; + } +} + +function localeKind(locale) { + if (locale instanceof Intl.Locale) { + return "locale"; + } else { + return locale; + } +} + +Mocha.describe("Untagged_runtime_annotations_test", () => { + Mocha.test("a subclass case is not taken for its superclass", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 98, characters 7-14", "animal", describePet(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 99, characters 7-14", "dog", describePet(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 100, characters 7-14", "rex", "rex"); + }); + Mocha.test("a superclass case alone excludes the subclass", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 104, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 105, characters 7-14", "other", onlyAnimal(new UntaggedDog())); + }); + Mocha.test("declaration order does not matter", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 109, characters 7-14", "dog", describeReversed(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 110, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); + }); + Mocha.test("a subclass upcasts to its superclass", () => { + let animal = new UntaggedDog(); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 115, characters 7-14", "dog", describePet(animal)); + }); + Mocha.test("classes reached through a module alias", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 119, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 120, characters 7-14", "dog", describeAliased(new UntaggedDog())); + }); + Mocha.test("typeOf symbol", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 124, characters 7-14", "symbol", keyKind(Symbol.for("key"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 125, characters 7-14", "text", "text"); + }); + Mocha.test("typeOf string", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 129, characters 7-14", "id", "id"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 130, characters 7-14", "index", "index"); + }); + Mocha.test("a class at a dotted path", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 134, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 135, characters 7-14", "de", "de"); + }); + Mocha.test("an annotated type is not wrapped in an option", () => { + let animal = new UntaggedAnimal(); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 140, characters 7-14", animal === animal); + }); +}); + +let ZooAlias; + +export { + describePet, + onlyAnimal, + describeReversed, + Zoo, + ZooAlias, + describeAliased, + keyKind, + referenceKind, + localeKind, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_runtime_annotations_test.res b/tests/tests/src/untagged_runtime_annotations_test.res new file mode 100644 index 00000000000..320d38cad84 --- /dev/null +++ b/tests/tests/src/untagged_runtime_annotations_test.res @@ -0,0 +1,142 @@ +open Mocha +open Test_utils + +%%raw(` +class Animal {} +class Dog extends Animal {} +globalThis.UntaggedAnimal = Animal +globalThis.UntaggedDog = Dog +`) + +@instanceOf("UntaggedAnimal") type animal +@instanceOf("UntaggedDog") type dog = private animal + +@new external makeAnimal: unit => animal = "UntaggedAnimal" +@new external makeDog: unit => dog = "UntaggedDog" + +@unboxed +type pet = Animal(animal) | Dog(dog) | Name(string) + +let describePet = pet => + switch pet { + | Animal(_) => "animal" + | Dog(_) => "dog" + | Name(name) => name + } + +let onlyAnimal = pet => + switch pet { + | Animal(_) => "animal" + | _ => "other" + } + +// The superclass case is declared after its subclass +@unboxed +type petReversed = DogFirst(dog) | AnimalSecond(animal) + +let describeReversed = pet => + switch pet { + | AnimalSecond(_) => "animal" + | DogFirst(_) => "dog" + } + +// Types reached through a module alias, as the stdlib's are +module Zoo = { + @instanceOf("UntaggedAnimal") type animal + @instanceOf("UntaggedDog") type dog = private animal + @new external makeDog: unit => dog = "UntaggedDog" +} +module ZooAlias = Zoo + +@unboxed +type aliased = AliasedAnimal(ZooAlias.animal) | AliasedDog(ZooAlias.dog) + +let describeAliased = pet => + switch pet { + | AliasedAnimal(_) => "animal" + | AliasedDog(_) => "dog" + } + +@typeOf("symbol") type sym +@val external symbolFor: string => sym = "Symbol.for" + +@unboxed +type key = Sym(sym) | Str(string) + +let keyKind = key => + switch key { + | Sym(_) => "symbol" + | Str(s) => s + } + +@typeOf("string") type id +external idOfString: string => id = "%identity" + +@unboxed +type reference = Id(id) | Index(int) + +let referenceKind = reference => + switch reference { + | Id(_) => "id" + | Index(_) => "index" + } + +@instanceOf("Intl.Locale") type locale +@new external makeLocale: string => locale = "Intl.Locale" + +@unboxed +type localeLike = Locale(locale) | Tag(string) + +let localeKind = locale => + switch locale { + | Locale(_) => "locale" + | Tag(tag) => tag + } + +describe(__MODULE__, () => { + test("a subclass case is not taken for its superclass", () => { + eq(__LOC__, "animal", describePet(Animal(makeAnimal()))) + eq(__LOC__, "dog", describePet(Dog(makeDog()))) + eq(__LOC__, "rex", describePet(Name("rex"))) + }) + + test("a superclass case alone excludes the subclass", () => { + eq(__LOC__, "animal", onlyAnimal(Animal(makeAnimal()))) + eq(__LOC__, "other", onlyAnimal(Dog(makeDog()))) + }) + + test("declaration order does not matter", () => { + eq(__LOC__, "dog", describeReversed(DogFirst(makeDog()))) + eq(__LOC__, "animal", describeReversed(AnimalSecond(makeAnimal()))) + }) + + test("a subclass upcasts to its superclass", () => { + let animal: animal = (makeDog() :> animal) + eq(__LOC__, "dog", describePet(Obj.magic(animal))) + }) + + test("classes reached through a module alias", () => { + eq(__LOC__, "animal", describeAliased(AliasedAnimal(Obj.magic(makeAnimal())))) + eq(__LOC__, "dog", describeAliased(AliasedDog(ZooAlias.makeDog()))) + }) + + test("typeOf symbol", () => { + eq(__LOC__, "symbol", keyKind(Sym(symbolFor("key")))) + eq(__LOC__, "text", keyKind(Str("text"))) + }) + + test("typeOf string", () => { + eq(__LOC__, "id", referenceKind(Id(idOfString("a")))) + eq(__LOC__, "index", referenceKind(Index(1))) + }) + + test("a class at a dotted path", () => { + eq(__LOC__, "locale", localeKind(Locale(makeLocale("en")))) + eq(__LOC__, "de", localeKind(Tag("de"))) + }) + + test("an annotated type is not wrapped in an option", () => { + let animal = makeAnimal() + ok(__LOC__, Obj.magic(Some(animal)) === animal) + }) +}) From 62fc5e393472bde7ed106d56860f45d66635cd8d Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 16:50:41 +0000 Subject: [PATCH 02/11] Check runtime annotations against the types they describe Why: An annotation on an abbreviation was trusted whatever the abbreviated type was. `@typeOf("string") type s = option` made `Some(None)` of `s` the same value as `None`, and two spellings of one type were recognized differently. Paths such as `Object`, `BigInt` or `globalThis.Date` were accepted and misrecognized every value or duplicated another class. An annotation on an abbreviation must now refine an abstract type that is not builtin, repeat the abbreviated type's annotation, or declare a subclass of the class annotated there. A subclass needs a private abbreviation, or a public one that is a strict instance of the superclass type (a phantom type hierarchy), and must not repeat a class of its chain. Paths that name no class to recognize values by are rejected. Payloads of `@unboxed` wrappers of annotated types are never undefined either, as with `@notUndefined`, and the function check excludes class cases as the object check does, so the two-arm form agrees with the `switch` form. Diagnostic added (annotation on a type that can be undefined): Example: @typeOf("string") type s = option After (diagnostic): @typeOf on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined. Diagnostic added (subclass on a public abbreviation): Example: @instanceOf("Animal") type animal @instanceOf("Dog") type dog = animal After (diagnostic): A subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion. Diagnostic added (class path): Example: @instanceOf("BigInt") type t After (diagnostic): Values of type bigint are primitives, not instances of BigInt. Use @typeOf("bigint") instead. JS modified (option of an unboxed wrapper): Example: @unboxed type boxedAnimal = BoxedAnimal(animal) let x: option = Some(BoxedAnimal(a)) Before (js): let x = Primitive_option.some(a); After (js): let x = a; JS modified (function case beside a class case): Example: @unboxed type handler = Function(int => int) | Callable(callable) Before (js): if (typeof handler === "function") { return "function"; } After (js): if (typeof handler === "function" && !(handler instanceof UntaggedCallable)) { return "function"; } Review: Why allow public subclasses that are strict instances? `Dom.res` and WebAPI model class hierarchies with phantom types, such as `type element<'a> = node<_element<'a>>`, whose subclass types unify with the superclass type only at the phantom argument. `type dog = animal` is the same type as `animal` and is rejected. Review: Why reject `globalThis.C` instead of normalizing it? The compiler already reads a shadowed class through `globalThis`, so the prefix adds nothing, and one spelling per class keeps the duplicate and subclass checks, which compare paths, exact. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/ast_untagged_variants.ml | 130 ++++++++++++++++-- compiler/ml/typedecl.ml | 6 + compiler/ml/typeopt.ml | 8 +- compiler/ml/variant_layout.ml | 60 +++++++- .../expected/instance_of_array.res.expected | 8 ++ ...tance_of_class_twice_in_chain.res.expected | 10 ++ .../instance_of_global_this.res.expected | 8 ++ .../instance_of_not_a_class.res.expected | 8 ++ .../expected/instance_of_object.res.expected | 8 ++ ...instance_of_primitive_wrapper.res.expected | 8 ++ .../instance_of_public_subclass.res.expected | 9 ++ ...notation_contradicts_manifest.res.expected | 9 ++ .../runtime_annotation_on_option.res.expected | 8 ++ ...e_annotation_on_type_variable.res.expected | 8 ++ .../fixtures/instance_of_array.res | 1 + .../instance_of_class_twice_in_chain.res | 3 + .../fixtures/instance_of_global_this.res | 1 + .../fixtures/instance_of_not_a_class.res | 1 + .../fixtures/instance_of_object.res | 1 + .../instance_of_primitive_wrapper.res | 1 + .../fixtures/instance_of_public_subclass.res | 2 + ...untime_annotation_contradicts_manifest.res | 2 + .../fixtures/runtime_annotation_on_option.res | 1 + .../runtime_annotation_on_type_variable.res | 1 + .../src/untagged_runtime_annotations_test.mjs | 62 ++++++--- .../src/untagged_runtime_annotations_test.res | 47 +++++++ 26 files changed, 373 insertions(+), 38 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/instance_of_array.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_class_twice_in_chain.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_global_this.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_not_a_class.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_object.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_primitive_wrapper.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_public_subclass.res.expected create mode 100644 tests/build_tests/super_errors/expected/runtime_annotation_contradicts_manifest.res.expected create mode 100644 tests/build_tests/super_errors/expected/runtime_annotation_on_option.res.expected create mode 100644 tests/build_tests/super_errors/expected/runtime_annotation_on_type_variable.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_array.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_class_twice_in_chain.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_global_this.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_not_a_class.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_object.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_primitive_wrapper.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_public_subclass.res create mode 100644 tests/build_tests/super_errors/fixtures/runtime_annotation_contradicts_manifest.res create mode 100644 tests/build_tests/super_errors/fixtures/runtime_annotation_on_option.res create mode 100644 tests/build_tests/super_errors/fixtures/runtime_annotation_on_type_variable.res diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index cc0029eb0a2..c859a3f77a9 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -14,6 +14,15 @@ type untagged_error = | DuplicateLiteral of string | SameLiteralValue of string * string | ConstructorMoreThanOneArg of string + +(* Why a syntactically valid [@instanceOf] path is still not accepted *) +and unsupported_class = + | Through_global_this + | Every_value_of of string (** names the [@typeOf] that says the same *) + | Wrapper_of of string (** names the [@typeOf] of the wrapped primitive *) + | Array_class + | Not_a_class + type error = | InvalidVariantAsAnnotation | VariantAsIntegerOutOfRange of string @@ -22,9 +31,14 @@ type error = | InvalidUntaggedVariantDefinition of untagged_error | TagFieldNameConflict of string * string * string | InvalidInstanceOf + | UnsupportedInstanceOf of string * unsupported_class | InvalidTypeOf of string | ConflictingRuntimeAnnotations | RuntimeAnnotationOnRepresentedType of string + | RuntimeAnnotationOnUnrelatedType of string + | RuntimeAnnotationContradictsManifest of string * string + | SubclassOfPublicAbbreviation of string * string + | ClassTwiceInChain of string exception Error of Location.t * error let report_error ppf = @@ -81,6 +95,30 @@ let report_error ppf = fprintf ppf "@instanceOf expects the name of a global class, such as \ @instanceOf(\"Date\") or @instanceOf(\"Intl.Locale\")." + | UnsupportedInstanceOf (name, reason) -> ( + match reason with + | Through_global_this -> + fprintf ppf + "Write @instanceOf(\"%s\") without \"globalThis.\": the compiler reads \ + the class through globalThis itself where a local binding shadows it." + (String.concat "." (List.tl (String.split_on_char '.' name))) + | Every_value_of type_of -> + fprintf ppf + "Every %s is an instance of %s, so @instanceOf(\"%s\") cannot tell \ + cases apart. Use @typeOf(\"%s\") instead." + type_of name name type_of + | Wrapper_of type_of -> + fprintf ppf + "Values of type %s are primitives, not instances of %s. Use \ + @typeOf(\"%s\") instead." + type_of name type_of + | Array_class -> + fprintf ppf + "Arrays are recognized by Array.isArray. Use array<_> as the payload \ + type instead of @instanceOf(\"Array\")." + | Not_a_class -> + fprintf ppf "%s is not a class, so it cannot be used with @instanceOf." + name) | InvalidTypeOf name -> fprintf ppf "@typeOf expects one of \"string\", \"number\", \"bigint\", \"boolean\", \ @@ -93,6 +131,27 @@ let report_error ppf = "@%s can only be used on abstract types and type abbreviations, whose \ runtime representation it describes." attribute + | RuntimeAnnotationOnUnrelatedType attribute -> + fprintf ppf + "@%s on a type abbreviation must describe an abstract type, or a type \ + with the same annotation. The abbreviated type already has a runtime \ + representation the compiler knows, or can be undefined." + attribute + | RuntimeAnnotationContradictsManifest (outer, inner) -> + fprintf ppf + "This annotation, %s, contradicts the abbreviated type's annotation, %s." + outer inner + | SubclassOfPublicAbbreviation (name, ancestor) -> + fprintf ppf + "A subclass must be a private abbreviation: write `= private ...` so \ + that instances of %s cannot be used as instances of %s without a \ + coercion." + ancestor name + | ClassTwiceInChain name -> + fprintf ppf + "%s appears twice in this chain of abbreviations, so it would be its own \ + superclass." + name (* Type of the runtime representation of a tag. @@ -176,6 +235,28 @@ type runtime_annotation = let instance_of = "instanceOf" let type_of = "typeOf" +(* The reason a global that is a valid path is still not a class to recognize + values by. Classes nested in namespaces, such as [Intl.Locale], are not + checked. *) +let unsupported_class name = + match String.split_on_char '.' name with + | "globalThis" :: _ :: _ -> Some Through_global_this + | [global] -> ( + match global with + | "Object" -> Some (Every_value_of "object") + | "Function" -> Some (Every_value_of "function") + | "Array" -> Some Array_class + | "String" -> Some (Wrapper_of "string") + | "Number" -> Some (Wrapper_of "number") + | "Boolean" -> Some (Wrapper_of "boolean") + | "BigInt" -> Some (Wrapper_of "bigint") + | "Symbol" -> Some (Wrapper_of "symbol") + | "globalThis" | "undefined" | "NaN" | "Infinity" | "Math" | "JSON" + | "Reflect" | "Atomics" | "Intl" | "Proxy" -> + Some Not_a_class + | _ -> None) + | _ -> None + let is_class_path name = let is_start = function | 'A' .. 'Z' | 'a' .. 'z' | '_' | '$' -> true @@ -206,6 +287,24 @@ let block_type_of_typeof = function | "object" -> Some ObjectType | _ -> None +let runtime_attribute_name = function + | Instance_of _ -> instance_of + | Type_of _ -> type_of + +let typeof_name = function + | StringType -> "string" + | IntType | FloatType -> "number" + | BigintType -> "bigint" + | BooleanType -> "boolean" + | SymbolType -> "symbol" + | FunctionType -> "function" + | ObjectType | InstanceType _ | UnknownType -> "object" + +let runtime_annotation_to_string = function + | Instance_of name -> Printf.sprintf "@%s(\"%s\")" instance_of name + | Type_of block_type -> + Printf.sprintf "@%s(\"%s\")" type_of (typeof_name block_type) + (* The [@instanceOf] or [@typeOf] annotation in [attrs], validated. Only [Typedecl] passes [~loc]; later readers see attributes it accepted. *) let process_runtime_annotation ?(loc = Location.none) @@ -214,7 +313,11 @@ let process_runtime_annotation ?(loc = Location.none) let loc = if attr_loc = Location.none then loc else attr_loc in if txt = instance_of then match Ast_payload.semantic_string_of_payload payload with - | Some name when is_class_path name -> Some (Instance_of name) + | Some name when is_class_path name -> ( + match unsupported_class name with + | None -> Some (Instance_of name) + | Some reason -> + raise (Error (loc, UnsupportedInstanceOf (name, reason)))) | _ -> raise (Error (loc, InvalidInstanceOf)) else if txt = type_of then match Ast_payload.semantic_string_of_payload payload with @@ -242,9 +345,7 @@ let check_runtime_annotation ~loc ~is_abstract_or_abbreviation (Error ( loc, RuntimeAnnotationOnRepresentedType - (match annotation with - | Instance_of _ -> instance_of - | Type_of _ -> type_of) )) + (runtime_attribute_name annotation) )) let type_is_builtin_object (t : Types.type_expr) = match t.desc with @@ -647,22 +748,21 @@ module Dynamic_checks = struct | InstanceType i -> Some i | _ -> None) in + (* Instances of a class can be objects or functions, so the cases of + those kinds exclude the class cases *) + let not_one_of_the_instances case = + Ext_list.fold_right instances case (fun i x -> + x &&& not (is_instance i y)) + in match tag_type with | Payload_shape ( IntType | StringType | FloatType | BigintType | BooleanType - | SymbolType | FunctionType ) -> + | SymbolType ) -> typeof y == x + | Payload_shape FunctionType -> not_one_of_the_instances (typeof y == x) | Payload_shape ObjectType -> - let object_case = - if has_null_case then typeof y == x &&& (y != nil) else typeof y == x - in - if instances <> [] then - let not_one_of_the_instances = - Ext_list.fold_right instances object_case (fun i x -> - x &&& not (is_instance i y)) - in - not_one_of_the_instances - else object_case + not_one_of_the_instances + (if has_null_case then typeof y == x &&& (y != nil) else typeof y == x) | Payload_shape (InstanceType i) -> is_case_instance ~instances i y | Payload_shape UnknownType -> (* This should not happen because unknown must be the only non-literal case *) diff --git a/compiler/ml/typedecl.ml b/compiler/ml/typedecl.ml index 12d49c809b9..3c79714d375 100644 --- a/compiler/ml/typedecl.ml +++ b/compiler/ml/typedecl.ml @@ -1612,6 +1612,11 @@ let transl_type_decl env rec_flag sdecl_list = | Type_abstract | Type_record _ | Type_open -> (id, decl)) decls in + List.iter2 + (fun sdecl (_, decl) -> + Variant_layout.check_manifest_annotation ~loc:sdecl.ptype_loc ~env:newenv + decl) + sdecl_list decls; List.iter2 (check_constraints ~type_record_as_object newenv) sdecl_list decls; (* Name recursion *) let decls = @@ -2019,6 +2024,7 @@ let transl_with_constraint env id row_path orig_decl sdecl = (match Ctype.closed_type_decl decl with | None -> () | Some ty -> raise (Error (sdecl.ptype_loc, Unbound_type_var (ty, decl)))); + Variant_layout.check_manifest_annotation ~loc:sdecl.ptype_loc ~env decl; let decl = name_recursion sdecl id decl in let type_variance = compute_variance_decl env true decl diff --git a/compiler/ml/typeopt.ml b/compiler/ml/typeopt.ml index 83e63378bdf..b0bec6b2900 100644 --- a/compiler/ml/typeopt.ml +++ b/compiler/ml/typeopt.ml @@ -39,10 +39,14 @@ let scrape env ty = (scrape_ty env ty).desc *) let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) = (* A value declared to be a class instance or to have a [typeof] other than - "undefined" is not undefined *) + "undefined" is not undefined. The payload of an [@unboxed] wrapper is + declared only after scraping. *) Option.is_some (Variant_layout.declared_block_type ~env typ) || - match scrape env typ with + let scraped = scrape_ty env typ in + Option.is_some (Variant_layout.declared_block_type ~env scraped) + || + match scraped.desc with | Tconstr (p, _, _) -> ( (* all built in types could not inhabit none-like values: int, char, float, bool, unit, exn, array, list, nativeint, diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index e57594c62b2..d97304818ea 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -40,10 +40,9 @@ let runtime_annotations ~env (t : Types.type_expr) = in collect ~previous:None t -(* The block type the outermost annotation of [t] states. A class's - ancestors are the classes annotated further down the chain. *) -let declared_block_type ~env t = - match runtime_annotations ~env t with +(* The block type the outermost of [annotations] states. A class's ancestors + are the classes annotated further down the chain. *) +let block_type_of_annotations = function | Instance_of name :: rest -> let ancestors = List.filter_map @@ -56,6 +55,59 @@ let declared_block_type ~env t = | Type_of block_type :: _ -> Some block_type | [] -> None +let declared_block_type ~env t = + block_type_of_annotations (runtime_annotations ~env t) + +(* An annotation on an abbreviation is a claim about the abbreviated type, so + it must agree with what the compiler knows about that type. It can refine + an abstract type, restate the annotation the abbreviated type has, or, on a + private abbreviation, declare a subclass of the class annotated there. + Anything else, such as [@typeOf("string") type t = option], would + let two spellings of one type be recognized differently, and would make + [Some(None)] of a type that can be undefined indistinguishable from + [None]. *) +let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = + match + (process_runtime_annotation decl.type_attributes, decl.type_manifest) + with + | None, _ | Some _, None -> () + | Some outer, Some manifest -> ( + let error err = raise (Error (loc, err)) in + match (outer, runtime_annotations ~env manifest) with + | _, [] -> ( + let is_abstract path = + match Env.find_type path env with + | {type_kind = Type_abstract; type_manifest = None} -> true + | _ -> false + | exception Not_found -> false + in + match (Ctype.expand_head_opt env manifest).desc with + | Tconstr (path, _, _) + when Predef.type_is_builtin_path_but_option path = Predef.NA + && is_abstract path -> + () + | _ -> + error (RuntimeAnnotationOnUnrelatedType (runtime_attribute_name outer))) + | Type_of block_type, Type_of inner :: _ when block_type = inner -> () + | Instance_of name, Instance_of inner :: _ when name = inner -> () + | Instance_of name, (Instance_of ancestor :: _ as chain) -> + (* A public abbreviation is the same type as the superclass, unless it + is a strict instance of it, as in the phantom type hierarchy + [type element<'a> = node<_element<'a>>] *) + let same_type = + match (Btype.repr manifest).desc with + | Tconstr (_, args, _) -> List.for_all Btype.is_Tvar args + | _ -> true + in + if decl.type_private = Public && same_type then + error (SubclassOfPublicAbbreviation (name, ancestor)) + else if List.mem outer chain then error (ClassTwiceInChain name) + | (Instance_of _ | Type_of _), inner :: _ -> + error + (RuntimeAnnotationContradictsManifest + ( runtime_annotation_to_string outer, + runtime_annotation_to_string inner ))) + let get_block_type_from_typ ~env (t : Types.type_expr) : block_type option = match declared_block_type ~env t with | Some _ as declared -> declared diff --git a/tests/build_tests/super_errors/expected/instance_of_array.res.expected b/tests/build_tests/super_errors/expected/instance_of_array.res.expected new file mode 100644 index 00000000000..6804e608a51 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_array.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_array.res:1:1-11 + + 1 │ @instanceOf("Array") type t + 2 │ + + Arrays are recognized by Array.isArray. Use array<_> as the payload type instead of @instanceOf("Array"). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_class_twice_in_chain.res.expected b/tests/build_tests/super_errors/expected/instance_of_class_twice_in_chain.res.expected new file mode 100644 index 00000000000..a7b94a69284 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_class_twice_in_chain.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/instance_of_class_twice_in_chain.res:3:23-42 + + 1 │ @instanceOf("Animal") type animal + 2 │ @instanceOf("Dog") type dog = private animal + 3 │ @instanceOf("Animal") type t = private dog + 4 │ + + Animal appears twice in this chain of abbreviations, so it would be its own superclass. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_global_this.res.expected b/tests/build_tests/super_errors/expected/instance_of_global_this.res.expected new file mode 100644 index 00000000000..0a0a8c96ff1 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_global_this.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_global_this.res:1:1-11 + + 1 │ @instanceOf("globalThis.Date") type t + 2 │ + + Write @instanceOf("Date") without "globalThis.": the compiler reads the class through globalThis itself where a local binding shadows it. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_not_a_class.res.expected b/tests/build_tests/super_errors/expected/instance_of_not_a_class.res.expected new file mode 100644 index 00000000000..2749054a98d --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_not_a_class.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_not_a_class.res:1:1-11 + + 1 │ @instanceOf("Math") type t + 2 │ + + Math is not a class, so it cannot be used with @instanceOf. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_object.res.expected b/tests/build_tests/super_errors/expected/instance_of_object.res.expected new file mode 100644 index 00000000000..6f28ce0dbf0 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_object.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_object.res:1:1-11 + + 1 │ @instanceOf("Object") type t + 2 │ + + Every object is an instance of Object, so @instanceOf("Object") cannot tell cases apart. Use @typeOf("object") instead. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_primitive_wrapper.res.expected b/tests/build_tests/super_errors/expected/instance_of_primitive_wrapper.res.expected new file mode 100644 index 00000000000..f62614d2a12 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_primitive_wrapper.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_primitive_wrapper.res:1:1-11 + + 1 │ @instanceOf("BigInt") type t + 2 │ + + Values of type bigint are primitives, not instances of BigInt. Use @typeOf("bigint") instead. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_public_subclass.res.expected b/tests/build_tests/super_errors/expected/instance_of_public_subclass.res.expected new file mode 100644 index 00000000000..207573475ef --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_public_subclass.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/instance_of_public_subclass.res:2:20-36 + + 1 │ @instanceOf("Animal") type animal + 2 │ @instanceOf("Dog") type dog = animal + 3 │ + + A subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/runtime_annotation_contradicts_manifest.res.expected b/tests/build_tests/super_errors/expected/runtime_annotation_contradicts_manifest.res.expected new file mode 100644 index 00000000000..fcc9121b913 --- /dev/null +++ b/tests/build_tests/super_errors/expected/runtime_annotation_contradicts_manifest.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/runtime_annotation_contradicts_manifest.res:2:19-30 + + 1 │ @typeOf("symbol") type sym + 2 │ @typeOf("number") type t = sym + 3 │ + + This annotation, @typeOf("number"), contradicts the abbreviated type's annotation, @typeOf("symbol"). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/runtime_annotation_on_option.res.expected b/tests/build_tests/super_errors/expected/runtime_annotation_on_option.res.expected new file mode 100644 index 00000000000..4da889368da --- /dev/null +++ b/tests/build_tests/super_errors/expected/runtime_annotation_on_option.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/runtime_annotation_on_option.res:1:19-41 + + 1 │ @typeOf("string") type t = option + 2 │ + + @typeOf on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/runtime_annotation_on_type_variable.res.expected b/tests/build_tests/super_errors/expected/runtime_annotation_on_type_variable.res.expected new file mode 100644 index 00000000000..fe18911eb10 --- /dev/null +++ b/tests/build_tests/super_errors/expected/runtime_annotation_on_type_variable.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/runtime_annotation_on_type_variable.res:1:19-33 + + 1 │ @typeOf("object") type t<'a> = 'a + 2 │ + + @typeOf on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/instance_of_array.res b/tests/build_tests/super_errors/fixtures/instance_of_array.res new file mode 100644 index 00000000000..a58cb3fc3aa --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_array.res @@ -0,0 +1 @@ +@instanceOf("Array") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_class_twice_in_chain.res b/tests/build_tests/super_errors/fixtures/instance_of_class_twice_in_chain.res new file mode 100644 index 00000000000..e860d395b52 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_class_twice_in_chain.res @@ -0,0 +1,3 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = private animal +@instanceOf("Animal") type t = private dog diff --git a/tests/build_tests/super_errors/fixtures/instance_of_global_this.res b/tests/build_tests/super_errors/fixtures/instance_of_global_this.res new file mode 100644 index 00000000000..6606ddd5eaf --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_global_this.res @@ -0,0 +1 @@ +@instanceOf("globalThis.Date") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_not_a_class.res b/tests/build_tests/super_errors/fixtures/instance_of_not_a_class.res new file mode 100644 index 00000000000..1d217e1c301 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_not_a_class.res @@ -0,0 +1 @@ +@instanceOf("Math") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_object.res b/tests/build_tests/super_errors/fixtures/instance_of_object.res new file mode 100644 index 00000000000..1db675c1fa8 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_object.res @@ -0,0 +1 @@ +@instanceOf("Object") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_primitive_wrapper.res b/tests/build_tests/super_errors/fixtures/instance_of_primitive_wrapper.res new file mode 100644 index 00000000000..d3673f71f0c --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_primitive_wrapper.res @@ -0,0 +1 @@ +@instanceOf("BigInt") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_public_subclass.res b/tests/build_tests/super_errors/fixtures/instance_of_public_subclass.res new file mode 100644 index 00000000000..b99153586fb --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_public_subclass.res @@ -0,0 +1,2 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = animal diff --git a/tests/build_tests/super_errors/fixtures/runtime_annotation_contradicts_manifest.res b/tests/build_tests/super_errors/fixtures/runtime_annotation_contradicts_manifest.res new file mode 100644 index 00000000000..3fb0d342b3e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/runtime_annotation_contradicts_manifest.res @@ -0,0 +1,2 @@ +@typeOf("symbol") type sym +@typeOf("number") type t = sym diff --git a/tests/build_tests/super_errors/fixtures/runtime_annotation_on_option.res b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_option.res new file mode 100644 index 00000000000..e70f7d33745 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_option.res @@ -0,0 +1 @@ +@typeOf("string") type t = option diff --git a/tests/build_tests/super_errors/fixtures/runtime_annotation_on_type_variable.res b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_type_variable.res new file mode 100644 index 00000000000..ccbb9e96f80 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/runtime_annotation_on_type_variable.res @@ -0,0 +1 @@ +@typeOf("object") type t<'a> = 'a diff --git a/tests/tests/src/untagged_runtime_annotations_test.mjs b/tests/tests/src/untagged_runtime_annotations_test.mjs index e0f43c4c14a..19be267ff0f 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.mjs +++ b/tests/tests/src/untagged_runtime_annotations_test.mjs @@ -7,6 +7,7 @@ class Animal {} class Dog extends Animal {} globalThis.UntaggedAnimal = Animal globalThis.UntaggedDog = Dog +globalThis.UntaggedCallable = class Callable extends Function {} ; function describePet(pet) { @@ -77,43 +78,68 @@ function localeKind(locale) { } } +function describePhantom(pet) { + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } else { + return "dog"; + } +} + +function handlerKind(handler) { + if (typeof handler === "function" && !(handler instanceof UntaggedCallable)) { + return "function"; + } else { + return "callable"; + } +} + Mocha.describe("Untagged_runtime_annotations_test", () => { Mocha.test("a subclass case is not taken for its superclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 98, characters 7-14", "animal", describePet(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 99, characters 7-14", "dog", describePet(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 100, characters 7-14", "rex", "rex"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 134, characters 7-14", "animal", describePet(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 135, characters 7-14", "dog", describePet(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 136, characters 7-14", "rex", "rex"); }); Mocha.test("a superclass case alone excludes the subclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 104, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 105, characters 7-14", "other", onlyAnimal(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 140, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 141, characters 7-14", "other", onlyAnimal(new UntaggedDog())); }); Mocha.test("declaration order does not matter", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 109, characters 7-14", "dog", describeReversed(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 110, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 145, characters 7-14", "dog", describeReversed(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 146, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); }); Mocha.test("a subclass upcasts to its superclass", () => { let animal = new UntaggedDog(); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 115, characters 7-14", "dog", describePet(animal)); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 151, characters 7-14", "dog", describePet(animal)); }); Mocha.test("classes reached through a module alias", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 119, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 120, characters 7-14", "dog", describeAliased(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 155, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 156, characters 7-14", "dog", describeAliased(new UntaggedDog())); }); Mocha.test("typeOf symbol", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 124, characters 7-14", "symbol", keyKind(Symbol.for("key"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 125, characters 7-14", "text", "text"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 160, characters 7-14", "symbol", keyKind(Symbol.for("key"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 161, characters 7-14", "text", "text"); }); Mocha.test("typeOf string", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 129, characters 7-14", "id", "id"); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 130, characters 7-14", "index", "index"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 165, characters 7-14", "id", "id"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 166, characters 7-14", "index", "index"); }); Mocha.test("a class at a dotted path", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 134, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 135, characters 7-14", "de", "de"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 170, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 171, characters 7-14", "de", "de"); }); Mocha.test("an annotated type is not wrapped in an option", () => { let animal = new UntaggedAnimal(); - Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 140, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 176, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 177, characters 7-14", animal === animal); + }); + Mocha.test("a phantom type hierarchy", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 181, characters 7-14", "animal", describePhantom(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 182, characters 7-14", "dog", describePhantom(new UntaggedDog())); + }); + Mocha.test("a function case excludes class instances", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 186, characters 7-14", "callable", handlerKind(new UntaggedCallable())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 187, characters 7-14", "function", handlerKind(x => x)); }); }); @@ -129,5 +155,7 @@ export { keyKind, referenceKind, localeKind, + describePhantom, + handlerKind, } /* Not a pure module */ diff --git a/tests/tests/src/untagged_runtime_annotations_test.res b/tests/tests/src/untagged_runtime_annotations_test.res index 320d38cad84..2c8f99d4d9f 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.res +++ b/tests/tests/src/untagged_runtime_annotations_test.res @@ -6,6 +6,7 @@ class Animal {} class Dog extends Animal {} globalThis.UntaggedAnimal = Animal globalThis.UntaggedDog = Dog +globalThis.UntaggedCallable = class Callable extends Function {} `) @instanceOf("UntaggedAnimal") type animal @@ -93,6 +94,41 @@ let localeKind = locale => | Tag(tag) => tag } +// A phantom type hierarchy declares subclasses with public abbreviations that +// are strict instances of their superclass type +type nodeLike<'a> +@instanceOf("UntaggedAnimal") type animalLike<'a> = nodeLike<'a> +type _dog<'a> +@instanceOf("UntaggedDog") type dogLike<'a> = animalLike<_dog<'a>> +type _base +@new external makePhantomAnimal: unit => animalLike<_base> = "UntaggedAnimal" +@new external makePhantomDog: unit => dogLike<_base> = "UntaggedDog" + +@unboxed +type phantomPet = PhantomAnimal(animalLike<_base>) | PhantomDog(dogLike<_base>) + +let describePhantom = pet => + switch pet { + | PhantomAnimal(_) => "animal" + | PhantomDog(_) => "dog" + } + +// Instances of a class can be functions +@instanceOf("UntaggedCallable") type callable +@new external makeCallable: unit => callable = "UntaggedCallable" + +@unboxed +type handler = Function(int => int) | Callable(callable) + +let handlerKind = handler => + switch handler { + | Callable(_) => "callable" + | Function(_) => "function" + } + +@unboxed +type boxedAnimal = BoxedAnimal(animal) + describe(__MODULE__, () => { test("a subclass case is not taken for its superclass", () => { eq(__LOC__, "animal", describePet(Animal(makeAnimal()))) @@ -138,5 +174,16 @@ describe(__MODULE__, () => { test("an annotated type is not wrapped in an option", () => { let animal = makeAnimal() ok(__LOC__, Obj.magic(Some(animal)) === animal) + ok(__LOC__, Obj.magic(Some(BoxedAnimal(animal))) === animal) + }) + + test("a phantom type hierarchy", () => { + eq(__LOC__, "animal", describePhantom(PhantomAnimal(makePhantomAnimal()))) + eq(__LOC__, "dog", describePhantom(PhantomDog(makePhantomDog()))) + }) + + test("a function case excludes class instances", () => { + eq(__LOC__, "callable", handlerKind(Callable(makeCallable()))) + eq(__LOC__, "function", handlerKind(Function(x => x))) }) }) From 152c9e110a1952d416a3727fea491525045a8218 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 16:51:00 +0000 Subject: [PATCH 03/11] Compare runtime annotations and untagged payloads across signatures Why: Interface inclusion ignored `@instanceOf`/`@typeOf`. An interface could claim a class its implementation does not have, or hide a private superclass manifest; the implementation and its clients then compiled different checks for the same values, and #8938 came back in the clients. Functor arguments, `with type` constraints and module type ascriptions had the same hole. When an interface states a class or `typeof` for a type, through its own annotation or the type it abbreviates, the implementation must state the same one, superclasses included. An interface that states none hides it. The payload kind of every untagged case is compared too, so a variant declared in both is recognized the same way on both sides. Diagnostic added (hidden superclass): Example: // Classes.res @instanceOf("Animal") type animal @instanceOf("Dog") type dog = private animal // Classes.resi @instanceOf("Animal") type animal @instanceOf("Dog") type dog After (diagnostic): Type declarations do not match: type dog = animal is not included in type dog Their runtime representations differ: the first declaration states instances of Dog, a subclass of Animal, the second declaration states instances of Dog. Diagnostic added (claim the implementation does not make): Example: // Claim.res type t = option // Claim.resi @typeOf("string") type t After (diagnostic): Their runtime representations differ: the first declaration states no runtime class or typeof, the second declaration states values whose typeof is "string". Review: Why compare payload kinds when the payload types are already compared? Equal payload types are classified alike only when both sides see the same annotations; an interface that hides one (stating none, which is allowed) gives its variant another layout. Review: Why not compare `@notUndefined` too? It has the same hole on master (#8964), fixed separately in #8966; the implied never-undefined of the new annotations is covered here, because the classification includes it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/ast_untagged_variants.ml | 11 ++++++ compiler/ml/includecore.ml | 34 ++++++++++++++++++- compiler/ml/includecore.mli | 1 + compiler/ml/variant_layout.ml | 16 +++++++++ ...e_annotation_claimed_by_interface.expected | 21 ++++++++++++ ...ntime_annotation_functor_argument.expected | 23 +++++++++++++ ...time_annotation_hidden_superclass.expected | 22 ++++++++++++ .../Claim.res | 1 + .../Claim.resi | 1 + .../A_classes.res | 2 ++ .../B_user.res | 10 ++++++ .../Classes.res | 2 ++ .../Classes.resi | 2 ++ 13 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_claimed_by_interface.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_functor_argument.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_superclass.expected create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/A_classes.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/B_user.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.resi diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index c859a3f77a9..67b3076ada1 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -305,6 +305,17 @@ let runtime_annotation_to_string = function | Type_of block_type -> Printf.sprintf "@%s(\"%s\")" type_of (typeof_name block_type) +(* What a declaration's annotations state, for error messages *) +let describe_declared_block_type = function + | Some (InstanceType (Class {name; ancestors = []})) -> "instances of " ^ name + | Some (InstanceType (Class {name; ancestors})) -> + "instances of " ^ name ^ ", a subclass of " + ^ String.concat " and " ancestors + | Some (InstanceType Array) -> "arrays" + | Some block_type -> + "values whose typeof is \"" ^ typeof_name block_type ^ "\"" + | None -> "no runtime class or typeof" + (* The [@instanceOf] or [@typeOf] annotation in [attrs], validated. Only [Typedecl] passes [~loc]; later readers see attributes it accepted. *) let process_runtime_annotation ?(loc = Location.none) diff --git a/compiler/ml/includecore.ml b/compiler/ml/includecore.ml index 712958ff8ba..e06683b891e 100644 --- a/compiler/ml/includecore.ml +++ b/compiler/ml/includecore.ml @@ -184,6 +184,7 @@ type type_mismatch = | Immediate | Tag_name | Variant_representation of Ident.t + | Runtime_annotation of string * string let report_type_mismatch0 first second decl ppf err = let pr fmt = Format.fprintf ppf fmt in @@ -216,6 +217,10 @@ let report_type_mismatch0 first second decl ppf err = | Tag_name -> pr "Their @tag annotations differ" | Variant_representation s -> pr "The internal representations for case %s are not equal" (Ident.name s) + | Runtime_annotation (declared1, declared2) -> + pr + "Their runtime representations differ:@ %s %s states %s,@ %s %s states %s" + first decl declared1 second decl declared2 let report_type_mismatch first second decl ppf = List.iter (fun err -> @@ -271,7 +276,16 @@ and compare_variants ~loc env params1 params2 ~layout1 ~layout2 n let tag_type2 = Variant_runtime.constructor_tag layout2 (n - 1) in if tag_type1 <> tag_type2 then [Variant_representation cd1.cd_id] else [] - | false -> r + | false -> ( + match + ( Variant_runtime.constructor_at layout1 (n - 1), + Variant_runtime.constructor_at layout2 (n - 1) ) + with + | ( Block (Untagged {block_type = block_type1}), + Block (Untagged {block_type = block_type2}) ) + when block_type1 <> block_type2 -> + [Variant_representation cd1.cd_id] + | _ -> r) in if r <> [] then r else @@ -317,6 +331,21 @@ and compare_records env params1_ params2_ n_ in aux ~fast:true params1_ params2_ n_ labels1_ labels2_ +(* Values are recognized by the class or [typeof] the second declaration + states, so the first must state the same. Stating nothing hides it. *) +let compare_runtime_annotations env decl1 decl2 = + match Variant_layout.declaration_block_type ~env decl2 with + | None -> [] + | Some _ as declared2 -> + let declared1 = Variant_layout.declaration_block_type ~env decl1 in + if declared1 = declared2 then [] + else + [ + Runtime_annotation + ( Ast_untagged_variants.describe_declared_block_type declared1, + Ast_untagged_variants.describe_declared_block_type declared2 ); + ] + let type_declarations ?(equality = false) ~loc env name decl1 id decl2 = Builtin_attributes.check_deprecated_inclusion ~def:decl1.type_loc ~use:decl2.type_loc loc decl1.type_attributes decl2.type_attributes name; @@ -342,6 +371,9 @@ let type_declarations ?(equality = false) ~loc env name decl1 id decl2 = if Ctype.equal env false [ty1] [ty2] then [] else [Manifest] else [Constraint] in + let err = + if err <> [] then err else compare_runtime_annotations env decl1 decl2 + in if err <> [] then err else let err = diff --git a/compiler/ml/includecore.mli b/compiler/ml/includecore.mli index ab6aebf6c06..6edd3bf32b3 100644 --- a/compiler/ml/includecore.mli +++ b/compiler/ml/includecore.mli @@ -38,6 +38,7 @@ type type_mismatch = | Immediate | Tag_name | Variant_representation of Ident.t + | Runtime_annotation of string * string val value_descriptions : loc:Location.t -> diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index d97304818ea..bfa6976b9aa 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -58,6 +58,22 @@ let block_type_of_annotations = function let declared_block_type ~env t = block_type_of_annotations (runtime_annotations ~env t) +(* The block type a declaration states, through its own annotation or the + ones of the type it abbreviates. *) +let declaration_block_type ~env (decl : Types.type_declaration) = + let own = process_runtime_annotation decl.type_attributes in + let inner = + match decl.type_manifest with + | Some manifest -> runtime_annotations ~env manifest + | None -> [] + in + let inner = + match inner with + | annotation :: rest when Some annotation = own -> rest + | _ -> inner + in + block_type_of_annotations (Option.to_list own @ inner) + (* An annotation on an abbreviation is a claim about the abbreviated type, so it must agree with what the compiler knows about that type. It can refine an abstract type, restate the annotation the abbreviated type has, or, on a diff --git a/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_claimed_by_interface.expected b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_claimed_by_interface.expected new file mode 100644 index 00000000000..1a1ffd0059a --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_claimed_by_interface.expected @@ -0,0 +1,21 @@ +===== Claim.res ===== + + We've found a bug for you! + /.../fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res:1:1-23 + + 1 │ type t = option + 2 │ + + The implementation /.../fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res + does not match the interface /.../fixtures/Cross_runtime_annotation_claimed_by_interface/claim.cmi: + Type declarations do not match: + type t = option + is not included in + type t + /.../fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.resi:1:19-24: + Expected declaration + /.../fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res:1:1-23: + Actual declaration + Their runtime representations differ: + the first declaration states no runtime class or typeof, + the second declaration states values whose typeof is "string". \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_functor_argument.expected b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_functor_argument.expected new file mode 100644 index 00000000000..c2f47fb74b0 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_functor_argument.expected @@ -0,0 +1,23 @@ +===== B_user.res ===== + + We've found a bug for you! + /.../fixtures/Cross_runtime_annotation_functor_argument/B_user.res:10:23-31 + + 8 │ } + 9 │ + 10 │ module Applied = Make(A_classes) + 11 │ + + Signature mismatch: + ... + Type declarations do not match: + type dog = A_classes.dog + is not included in + type dog + /.../fixtures/Cross_runtime_annotation_functor_argument/B_user.res:4:24-31: + Expected declaration + /.../fixtures/Cross_runtime_annotation_functor_argument/A_classes.res:2:20-44: + Actual declaration + Their runtime representations differ: + the first declaration states instances of Dog, a subclass of Animal, + the second declaration states instances of Dog. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_superclass.expected b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_superclass.expected new file mode 100644 index 00000000000..484244fad91 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_superclass.expected @@ -0,0 +1,22 @@ +===== Classes.res ===== + + We've found a bug for you! + /.../fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res:2:20-44 + + 1 │ @instanceOf("Animal") type animal + 2 │ @instanceOf("Dog") type dog = private animal + 3 │ + + The implementation /.../fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res + does not match the interface /.../fixtures/Cross_runtime_annotation_hidden_superclass/classes.cmi: + Type declarations do not match: + type dog = private animal + is not included in + type dog + /.../fixtures/Cross_runtime_annotation_hidden_superclass/Classes.resi:2:20-27: + Expected declaration + /.../fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res:2:20-44: + Actual declaration + Their runtime representations differ: + the first declaration states instances of Dog, a subclass of Animal, + the second declaration states instances of Dog. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res new file mode 100644 index 00000000000..aae9f434a88 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.res @@ -0,0 +1 @@ +type t = option diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.resi b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.resi new file mode 100644 index 00000000000..c9c5b03fc5d --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_claimed_by_interface/Claim.resi @@ -0,0 +1 @@ +@typeOf("string") type t diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/A_classes.res b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/A_classes.res new file mode 100644 index 00000000000..793a12dbd3b --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/A_classes.res @@ -0,0 +1,2 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = private animal diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/B_user.res b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/B_user.res new file mode 100644 index 00000000000..0e21ff293c8 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_functor_argument/B_user.res @@ -0,0 +1,10 @@ +module Make = ( + X: { + @instanceOf("Animal") type animal + @instanceOf("Dog") type dog + }, +) => { + @unboxed type t = A(X.animal) | D(X.dog) +} + +module Applied = Make(A_classes) diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res new file mode 100644 index 00000000000..793a12dbd3b --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.res @@ -0,0 +1,2 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = private animal diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.resi b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.resi new file mode 100644 index 00000000000..14d2defccf2 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_superclass/Classes.resi @@ -0,0 +1,2 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog From c1af61827ddd1a1d4639057845bbb0fd6a348d53 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 16:51:23 +0000 Subject: [PATCH 04/11] Document runtime annotations in the untagged variants spec Why: The spec states the rules the compiler implements, and its open questions asked what a mechanism for declaring classes and kinds on types has to decide. Rule 5 now describes `@instanceOf` and `@typeOf`: where they can appear, how they are read along a chain of abbreviations, which abbreviations and class paths are rejected, and the name list that still covers types without annotations. Rules 4, 6, 8, 10, 12, 13 and 14 gain what the annotations change: declared kinds, one case per class and per symbol, subclass and function exclusions, missing classes, implied never-undefined, and inclusion checks. The open questions become the decisions taken, with what is still open. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/UntaggedVariants.md | 235 ++++++++++++++++++++++---------- 1 file changed, 166 insertions(+), 69 deletions(-) diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index a3e21a83529..8c7cd9df895 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -90,26 +90,87 @@ into one kind: | a function type | function | `typeof x === "function"` | | a record (not `@unboxed`), an inline record, `dict<_>` | object | `typeof x === "object"` | | `array<_>`, a tuple | instance `Array` | `Array.isArray(x)` | -| a type in the list of rule 5 | instance `C` | `x instanceof C` | +| a type declared with `@instanceOf("C")`, or listed in rule 5 | instance `C` | `x instanceof C` | +| a type declared with `@typeOf("k")` | kind `k` | `typeof x === "k"` | | anything else: an abstract type, a type variable, a variant, `list<_>`, an `@unboxed` record | unknown | no check | -The payload type is classified after expanding abbreviations -(`type myDate = Date.t` is an instance case), except that the list of rule 5 -is consulted first on the type as written. `@notUndefined` on an abstract type -leaves it unknown. +The annotations of rule 5 are read first, on the type as written and on each +type it abbreviates. Then the list of rule 5 is consulted on the type as +written, and then the payload type is classified after expanding +abbreviations (`type myDate = Date.t` is an instance case). `@typeOf("number")` +is the number kind, and `@typeOf("symbol")` is a kind of its own, with no +builtin payload type. `@notUndefined` on an abstract type leaves it unknown. - Implementation: `variant_layout.ml:get_block_type_from_typ`, - `get_block_type`; `ast_untagged_variants.ml:type_is_builtin_object`. + `get_block_type`, `declared_block_type`; + `ast_untagged_variants.ml:type_is_builtin_object`. - Tests: `spec_test` rules 4 (`kinds`, `tuples`, `aliasDict`, `instances`, `unknown`); fixtures `UntaggedConstructorMoreThanOneArg.res`, `UntaggedConstructorAttributeMoreThanOneArg.res`, `UntaggedNonUnary1.res`, `UntaggedNonUnary2.res` ("Constructor A has more than one argument."), - `UntaggedUnknown.res` (`list` is unknown). + `UntaggedUnknown.res` (`list` is unknown); + `tests/tests/src/untagged_runtime_annotations_test.res`. - Shaped by #6121, #6208, #6219, #6279, #6368, #6394, #6550, #6670. -### 5. Instance types +### 5. Declared and listed instance types -The instance cases come from a fixed list of type names in +`@instanceOf("C")` on a type declares that its values are instances of the +global class at the path `C`, such as `"Date"` or `"Intl.Locale"`. +`@typeOf("k")` declares that their `typeof` is `k`, one of `"string"`, +`"number"`, `"bigint"`, `"boolean"`, `"symbol"`, `"function"` and +`"object"`. A type has at most one of the two, and only abstract types and +type abbreviations, public or private, can have them. + +The classifier reads the annotations of the payload type as written and of +each type it abbreviates; the outermost one decides. The classes annotated +further down a chain of abbreviations are the superclasses of the outermost +one (rule 8): + +```rescript +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = private animal +``` + +A module alias repeats the declarations of the aliased module, so an +annotation equal to the one just read is not counted again. + +An annotation on an abbreviation must agree with the abbreviated type. It can +refine an abstract type that is not builtin and has no annotation, or repeat +the annotation of the abbreviated type. Only a private abbreviation can +declare a subclass of the class annotated there, or a public one that is a +strict instance of the superclass type, as in the phantom type hierarchy +`@instanceOf("Element") type elementLike<'a> = nodeLike<_element<'a>>`. A +public `type dog = animal` would make every animal a dog without a coercion. + +| Declaration | Error | Fixture | +| --- | --- | --- | +| `@typeOf("string") type t = option`, `type t<'a> = 'a`, an abbreviation of `unit`, a builtin type, a record or a variant | @typeOf on a type abbreviation must describe an abstract type, ... | `runtime_annotation_on_option.res`, `runtime_annotation_on_type_variable.res` | +| `@typeOf("number") type t = sym` with `@typeOf("symbol") type sym` | This annotation, @typeOf("number"), contradicts the abbreviated type's annotation, @typeOf("symbol"). | `runtime_annotation_contradicts_manifest.res` | +| `@instanceOf("Dog") type dog = animal` | A subclass must be a private abbreviation ... | `instance_of_public_subclass.res` | +| `@instanceOf("Animal") type t = private dog` | Animal appears twice in this chain of abbreviations ... | `instance_of_class_twice_in_chain.res` | +| `@instanceOf` on a record or variant | @instanceOf can only be used on abstract types and type abbreviations ... | `runtime_annotation_on_record.res` | +| both annotations | A type can have either @instanceOf or @typeOf, not both. | `runtime_annotation_conflict.res` | + +The class path is a dot-separated list of identifiers whose first one is not +a JavaScript keyword. Paths that name no class to recognize values by are +rejected as well: + +| Path | Error | Fixture | +| --- | --- | --- | +| not a path, or a keyword global | @instanceOf expects the name of a global class ... | `instance_of_invalid_class.res`, `instance_of_keyword.res` | +| `globalThis.C` | Write @instanceOf("C") without "globalThis." ... | `instance_of_global_this.res` | +| `Object`, `Function` | Every object is an instance of Object ... Use @typeOf("object") instead. | `instance_of_object.res` | +| `Array` | Arrays are recognized by Array.isArray ... | `instance_of_array.res` | +| `String`, `Number`, `Boolean`, `BigInt`, `Symbol` | Values of type bigint are primitives ... | `instance_of_primitive_wrapper.res` | +| `globalThis`, `undefined`, `NaN`, `Infinity`, `Math`, `JSON`, `Reflect`, `Atomics`, `Intl`, `Proxy` | Math is not a class ... | `instance_of_not_a_class.res` | + +An unknown `typeof` is an error too (`type_of_invalid.res`). The class is read +as a global; where a local binding of the same name is in scope, it is read +through `globalThis` (#8785). The compiler trusts the annotations, as it +trusts the type of an external: a value that is not what its type declares is +recognized by what it is (rule 10). + +Types without annotations are recognized from a fixed list of type names in `ast_untagged_variants.ml:type_to_instanceof_backed_obj`. The predefined `promise<_>` and `array<_>` are matched by path; the others by the printed path of the type: @@ -122,15 +183,21 @@ path of the type: | `Set.t`, `Map.t`, `WeakSet.t`, `WeakMap.t` | the class of the same name | | `Int8Array.t`, `Uint8Array.t`, `Uint8ClampedArray.t`, `Int16Array.t`, `Uint16Array.t`, `Int32Array.t`, `Uint32Array.t`, `Float32Array.t`, `Float64Array.t`, `BigInt64Array.t`, `BigUint64Array.t` | the class of the same name | -A type outside this list, such as a binding to a Web API class, is unknown -(rule 4). The typed-array types are abbreviations of one type, -`TypedArray.t<_>`, and are told apart only by the name as written. - -- Implementation: `ast_untagged_variants.ml:type_to_instanceof_backed_obj`, - `variant_runtime.ml:Instance`. -- Tests: `spec_test` rules 4 and 5; fixtures `UntaggedAtMostOneInstance.res`, - `UntaggedTupleAndArray.res`. -- Shaped by #6296, #6383, #6396, #7788, #8009. Related: #7147. +The listed classes have no superclasses. The typed-array types are +abbreviations of one type, `TypedArray.t<_>`, and are told apart only by the +name as written. + +- Implementation: `variant_layout.ml:runtime_annotations`, + `declared_block_type`, `check_manifest_annotation`; + `ast_untagged_variants.ml:process_runtime_annotation`, `unsupported_class`, + `check_runtime_annotation`, + `type_to_instanceof_backed_obj`; + `variant_runtime.ml:Instance`; `js_exp_make.ml:class_constructor`. +- Tests: `spec_test` rules 4 and 5; + `tests/tests/src/untagged_runtime_annotations_test.res`, + `untagged_runtime_annotations_shadow_test.res`; the fixtures above, + `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res`. +- Shaped by #6296, #6383, #6396, #7788, #8009, #8950. Related: #7147. ### 6. Well-formedness @@ -140,12 +207,13 @@ The type checker rejects an untagged variant, with | Condition | Reason | Fixture | | --- | --- | --- | | two object cases | At most one case can be an object type. | `UntaggedAtMostOneObject.res` | -| two cases of one instance class (arrays and tuples share `Array`) | At most one case can be a C type; for `Array`, an array or tuple type. | `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res` | +| two cases of one instance class, compared by path (arrays and tuples share `Array`) | At most one case can be an instance of C; for `Array`, an array or tuple type. | `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res`, `untagged_same_class_twice.res` | | two function cases | At most one case can be a function type. | `UntaggedAtMostOneFunction.res` | | two string cases | At most one case can be a string type. | `UntaggedAtMostOneString.res` | | two number cases (`int` and `float` count together) | At most one case can be a number type (int or float). | `UntaggedAtMostOneNumber.res` | | two bigint cases | At most one case can be a bigint type. | `UntaggedAtMostOneBigint.res` | | two boolean cases | At most one case can be a boolean type. | `UntaggedAtMostOneBoolean.res` | +| two symbol cases | At most one case can be a symbol type. | `untagged_two_symbols.res` | | an unknown case beside another case with a payload | Case A has a payload that is not of one of the recognized shapes ... | `UntaggedOnlyOneUnknown.res`, `UntaggedUnknown.res` | | a `bool` case when both `true` and `false` are literal cases | Case B can never match ... | `UntaggedUnreachableBoolean.res` | | two nullary cases with the same literal | Duplicate literal x. | `UntaggedDuplicateLiteral.res` | @@ -208,21 +276,37 @@ The payload arms compile in one of two forms. declared first, with the other branch unchecked (rule 10). The object check is `typeof x === "object"`, followed by `x !== null` when the variant has an `@as(null)` case, and by `!(x instanceof C)` (`!Array.isArray(x)` for - arrays) for every instance case of the variant. + arrays) for every instance case of the variant. The function check is + followed by `!(x instanceof C)` for every class case. - **Three or more arms**: an `if` chain of the instance checks, in the declaration order of the cases, then one `switch (typeof x)` over the other arms. -In both forms a value of an instance case never lands in the object case. +In both forms a value of an instance case never lands in the object or +function case. A class check is followed by `!(x instanceof D)` for every +case whose class `D` is a subclass of it (rule 5), so a subclass instance +never lands in its superclass's case, whatever the declaration order: + +```js +if (v instanceof Animal && !(v instanceof Dog)) { ... } +``` + +A value upcast to the superclass and wrapped in its case lands in the +subclass case, as a payload equal to a literal lands in the literal's case +(rule 7). Classes not declared to be related are tested in declaration order. +`instanceof` consults `Symbol.hasInstance`, which is trusted. An `int` case is recognized by `typeof x === "number"` and so receives every number, integral or not. - Implementation: `lam_compile.ml:compile_untagged_cases`, `compile_general_cases`; - `ast_untagged_variants.ml:Dynamic_checks.add_runtime_type_check`. + `ast_untagged_variants.ml:Dynamic_checks.add_runtime_type_check`, + `is_case_instance`. - Tests: `spec_test` rules 4, 5 and 8 (`instanceName` shows the chain in declaration order, `objectFirst` both forms); `tests/tests/src/UntaggedVariants.res` - (`AllInstanceofTypes`, `MergeCases`). + (`AllInstanceofTypes`, `MergeCases`); + `tests/tests/src/untagged_runtime_annotations_test.res` (subclasses, the + function case). - Shaped by #6103, #6383, #6792, #7144, #7149. ### 9. `null` and arrays are not objects @@ -232,7 +316,9 @@ number, integral or not. when the variant has an array or tuple case: the two-arm form adds the checks of rule 8, and the `switch (typeof x)` form is preceded by `x === null` / `Array.isArray(x)` guards that send those values to the default -arm when their own arm is absent. +arm when their own arm is absent. A case declared with `@typeOf("object")` +(rule 5) is an object case: without an `@as(null)` case, it receives `null`, +as a record case does. | Declaration | `switch` arms | Value | Arm | | --- | --- | --- | --- | @@ -258,7 +344,10 @@ Recognition trusts the type. A value that belongs to no case is not rejected: completes without running any arm. A value from another JavaScript realm fails every `instanceof` check and is -recognized the same way (by the semantics of `instanceof`; not tested). +recognized the same way (by the semantics of `instanceof`; not tested): +recognition is local to a realm. A class that does not exist where the +`switch` runs makes its check throw, for every value: the instance checks +precede the `typeof` checks, so primitives reach them too. - Implementation: `lam_compile.ml:compile_general_cases` (a complete match emits no check for its last clause). @@ -287,7 +376,7 @@ as `Primitive_option.some(v)` otherwise. An untagged variant cannot hold | `string`, `int`, `float`, `bool`, `bigint`, a function, a tuple, a record without `@unboxed` | no | | an inline record with two or more fields | no | | an `@unboxed` record, an inline record with one field | as the type of its field | -| an abstract type with `@notUndefined` (rule 13) | no | +| an abstract type with `@notUndefined` (rule 13), a type declared with `@instanceOf` or `@typeOf` (rule 5), an `@unboxed` wrapper of either | no | | an abstract type without it, a type variable, `option<_>` | yes | | a type whose declaration is not on the load path | yes | @@ -309,6 +398,9 @@ The stdlib marks `Date.t`, `RegExp.t`, `Map.t`, `Set.t`, `WeakMap.t`, `WeakSet.t`, `ArrayBuffer.t`, `DataView.t`, the typed arrays and other abstract types `@notUndefined`; `Blob.t` and `File.t` are not marked. +Rule 5 keeps an annotated abbreviation from claiming this of a type that can +hold `undefined`. + - Implementation: `typeopt.ml:type_cannot_contain_undefined`. - Tests: `spec_test` rule 12; `UntaggedVariants.res` (`OptionUnboxingHeuristic`); `tests/tests/src/option_wrapping_test.res`; @@ -322,7 +414,8 @@ values are never `undefined`. It affects only rule 12: `option` of the type, and of an untagged variant with a payload of the type, is unboxed. It does not make the type a recognized payload kind (rule 4). On any other type declaration it is an error, "@notUndefined can only be used on abstract -types". Interface inclusion does not compare it. +types". Interface inclusion does not compare it. `@instanceOf` and `@typeOf` +(rule 5) imply it. - Implementation: `typedecl.ml:transl_declaration`, `is_not_undefined_attr`; `typeopt.ml:type_cannot_contain_undefined`. @@ -337,9 +430,16 @@ with a payload (`TAG` by default), and `@as` on such a case sets the tag's value. An inline-record field whose runtime name equals the tag field is an error. -An interface and its implementation must agree on `@unboxed`, on `@tag`, and -on the literal of every nullary case. They are not compared on the `@as` of -cases with a payload (see Known bugs). +An interface and its implementation must agree on `@unboxed`, on `@tag`, on +the literal of every nullary case, and on the payload kind of every untagged +case. They are not compared on the `@as` of cases with a payload (see Known +bugs). + +A type for which an interface states a class or `typeof` (rule 5), through +its own annotation or the type it abbreviates, must state the same one in the +implementation, superclasses included: "Their runtime representations +differ". An interface that states none hides it. The same holds for functor +arguments, `with type` constraints and module type ascriptions. - Implementation: `variant_layout.ml:layout_from_type_variant`, `ast_untagged_variants.ml:process_tag_name`, `check_tag_field_conflicts`; @@ -348,7 +448,10 @@ cases with a payload (see Known bugs). `variant_tag_overlaps_with_field.res`, `UntaggedInvalidVariantTagAnnotation.res`, `UntaggedImplIntf.res`; `tests/build_tests/super_errors_multi/fixtures/` `Iface_unboxed_variant_mismatch`, `Iface_tag_name_mismatch`, - `Iface_variant_representation_mismatch`. + `Iface_variant_representation_mismatch`, + `Cross_runtime_annotation_hidden_superclass`, + `Cross_runtime_annotation_claimed_by_interface`, + `Cross_runtime_annotation_functor_argument`. - Shaped by #6498, #6513, #6669, #7875. ## Known bugs @@ -404,41 +507,35 @@ module M: { Console.log(switch M.a { | M.A(_) => "A" | M.C(_) => "C" }) // C ``` -## Open questions - -These are the decisions a general mechanism for recognizing payloads by class -or `typeof` kind, declared on types rather than listed in the compiler (rule -5), has to make. - -1. **Unchecked claims.** Is a declared class or kind checked against the type - it is declared on, or trusted? An unknown payload must be the only payload - case (rule 6); a recognized one may sit beside others, so a wrong claim - moves values between cases. Related: #7147. -2. **Subclasses.** How does the compiler learn that one class extends another, - and does a superclass case exclude a subclass case of the same variant - regardless of declaration order (Known bugs, #8938)? What happens to a - value upcast to the superclass and wrapped in its case (rule 7 has the same - effect for literals)? Related: #8806, #8914. -3. **Overlaps across kinds.** Rule 6 counts cases per kind and per class. - Which overlaps are rejected or ordered: a class and the object case - (`Object`), a class and the function case (`Function`), two unrelated - classes, a declared `typeof` kind and a primitive case or literal? - Related: #6123, #6207, #7289, #8251. -4. **The unchecked branch.** Rule 10 sends values outside every case to the - last case or to no arm. Is that the contract, or is the last case checked - too? Related: #6103. -5. **Realms.** `instanceof` is false across realms and overridable through - `Symbol.hasInstance`; arrays use `Array.isArray`. Is realm-local - recognition part of the contract? Related: #6103, #2117. -6. **Interfaces and functors.** Inclusion compares `@unboxed` and tags (rule - 14) but not `@notUndefined`. Does it compare declared classes and kinds? - Related: #6498, #6513, #6669. -7. **Implied `@notUndefined`.** Does a declared class or kind imply that the - type never holds `undefined` (rule 12), and how is that kept consistent - with the declared type's own structure? Related: #6222, #6233, #7458. -8. **Abbreviations.** Rule 5 reads the name as written before expanding; two - equal types classify differently (Known bugs). Does a declaration on an - abbreviation follow the same rule? Related: #6394, #7788. -9. **Globals.** Classes are read as globals. How is a class shadowed by a - local binding, imported from a module, or missing at run time handled? - Related: #8785. +## Decisions on declared classes and kinds + +The questions a mechanism for declaring classes and `typeof` kinds on types +had to answer, and the answers of rule 5: + +1. **Unchecked claims.** An annotation is trusted, as the type of an external + is, but checked against the type it abbreviates (rule 5). +2. **Subclasses.** A class's superclasses are the classes annotated further + down its chain of private abbreviations, or of strict instances in a + phantom type hierarchy. A superclass case excludes its subclass cases in + any order, and an upcast value lands in the subclass case (rule 8). The + relation must be visible where the variant is declared: an interface that + hides it is rejected (rule 14). +3. **Overlaps across kinds.** `Object`, `Function`, `Array` and the wrappers + of primitives are rejected as classes. A declared kind counts as the + builtin kind (rule 6). Unrelated classes are tested in declaration order. +4. **The unchecked branch** stays the contract (rule 10). +5. **Realms.** Recognition is local to a realm (rule 10). +6. **Interfaces and functors** compare declared classes and kinds, and the + payload kinds of untagged cases (rule 14). They still do not compare + `@notUndefined`. +7. **Implied `@notUndefined`.** A declared class or kind implies it, and + rule 5 rejects annotations on abbreviations of types that can hold + `undefined`. +8. **Abbreviations.** Annotations are read along the chain of abbreviations; + the list of rule 5 is still read on the name as written. +9. **Globals.** A shadowed class is read through `globalThis`. A missing + class throws when the check runs (rule 10). + +Still open: a class imported from a module cannot be named; the `typeof` +checks could precede the instance checks, so that a missing class affects +only objects; and the typed arrays share one type (#8955). From 858a5c46968008d6d2d9a167a067c8c50ae0044a Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 21 Oct 2026 18:01:54 +0000 Subject: [PATCH 05/11] Annotate the stdlib's runtime classes Why: Untagged variants recognized stdlib classes only by type name, with no relation between them, so a `Blob` case declared before a `File` case also caught files (#8938). Types whose values are instances of a global class now say so with `@instanceOf` instead of `@notUndefined`, `Symbol.t` with `@typeOf("symbol")`, and `File.t` is a private abbreviation of `Blob.t`. `JsError.t`, `Symbol.t` and the `Intl` classes can be untagged variant cases too. Fixes #8938. JS modified (Blob and File cases): Example: @unboxed type upload = Blob(Blob.t) | File(File.t) let kind = upload => switch upload { | Blob(_) => "blob" | File(_) => "file" } Before (js): if (upload instanceof Blob) { return "blob"; } else { return "file"; } After (js): if (upload instanceof Blob && !(upload instanceof File)) { return "blob"; } else { return "file"; } Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- changes/stdlib-instanceof-annotations.fix.md | 1 + .../@rescript/runtime/Stdlib_ArrayBuffer.res | 2 +- .../@rescript/runtime/Stdlib_ArrayBuffer.resi | 2 +- .../runtime/Stdlib_BigInt64Array.res | 2 +- .../runtime/Stdlib_BigUint64Array.res | 2 +- packages/@rescript/runtime/Stdlib_Blob.res | 1 + .../@rescript/runtime/Stdlib_DataView.res | 2 +- .../@rescript/runtime/Stdlib_DataView.resi | 2 +- packages/@rescript/runtime/Stdlib_Date.res | 2 +- packages/@rescript/runtime/Stdlib_Date.resi | 2 +- packages/@rescript/runtime/Stdlib_File.res | 5 ++- .../@rescript/runtime/Stdlib_Float32Array.res | 2 +- .../@rescript/runtime/Stdlib_Float64Array.res | 2 +- .../@rescript/runtime/Stdlib_Int16Array.res | 2 +- .../@rescript/runtime/Stdlib_Int32Array.res | 2 +- .../@rescript/runtime/Stdlib_Int8Array.res | 2 +- .../runtime/Stdlib_Intl_Collator.res | 2 +- .../runtime/Stdlib_Intl_DateTimeFormat.res | 2 +- .../runtime/Stdlib_Intl_ListFormat.res | 2 +- .../@rescript/runtime/Stdlib_Intl_Locale.res | 2 +- .../runtime/Stdlib_Intl_NumberFormat.res | 2 +- .../runtime/Stdlib_Intl_PluralRules.res | 2 +- .../Stdlib_Intl_RelativeTimeFormat.res | 2 +- .../runtime/Stdlib_Intl_Segmenter.res | 2 +- packages/@rescript/runtime/Stdlib_JsError.res | 2 +- .../@rescript/runtime/Stdlib_JsError.resi | 2 +- packages/@rescript/runtime/Stdlib_Map.res | 2 +- packages/@rescript/runtime/Stdlib_Map.resi | 2 +- packages/@rescript/runtime/Stdlib_RegExp.res | 2 +- packages/@rescript/runtime/Stdlib_RegExp.resi | 2 +- packages/@rescript/runtime/Stdlib_Set.res | 2 +- packages/@rescript/runtime/Stdlib_Set.resi | 2 +- packages/@rescript/runtime/Stdlib_Symbol.res | 2 +- packages/@rescript/runtime/Stdlib_Symbol.resi | 2 +- .../@rescript/runtime/Stdlib_Uint16Array.res | 2 +- .../@rescript/runtime/Stdlib_Uint32Array.res | 2 +- .../@rescript/runtime/Stdlib_Uint8Array.res | 2 +- .../runtime/Stdlib_Uint8ClampedArray.res | 2 +- packages/@rescript/runtime/Stdlib_WeakMap.res | 2 +- packages/@rescript/runtime/Stdlib_WeakSet.res | 2 +- tests/tests/src/UntaggedVariants.mjs | 2 +- tests/tests/src/untagged_file_blob_test.mjs | 41 +++++++++++++++++++ tests/tests/src/untagged_file_blob_test.res | 38 +++++++++++++++++ 43 files changed, 122 insertions(+), 40 deletions(-) create mode 100644 changes/stdlib-instanceof-annotations.fix.md create mode 100644 tests/tests/src/untagged_file_blob_test.mjs create mode 100644 tests/tests/src/untagged_file_blob_test.res diff --git a/changes/stdlib-instanceof-annotations.fix.md b/changes/stdlib-instanceof-annotations.fix.md new file mode 100644 index 00000000000..672e8786031 --- /dev/null +++ b/changes/stdlib-instanceof-annotations.fix.md @@ -0,0 +1 @@ +- An untagged variant with both `Blob` and `File` cases matches files as `File`, whatever the order of its cases. `File.t` is now a private abbreviation of `Blob.t`, and `JsError.t`, `Symbol.t` and the `Intl` classes can be untagged variant cases. diff --git a/packages/@rescript/runtime/Stdlib_ArrayBuffer.res b/packages/@rescript/runtime/Stdlib_ArrayBuffer.res index 4983aff64d8..24773b6bce8 100644 --- a/packages/@rescript/runtime/Stdlib_ArrayBuffer.res +++ b/packages/@rescript/runtime/Stdlib_ArrayBuffer.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("ArrayBuffer") type t @new external make: int => t = "ArrayBuffer" diff --git a/packages/@rescript/runtime/Stdlib_ArrayBuffer.resi b/packages/@rescript/runtime/Stdlib_ArrayBuffer.resi index e74dd6667f1..f45360bb879 100644 --- a/packages/@rescript/runtime/Stdlib_ArrayBuffer.resi +++ b/packages/@rescript/runtime/Stdlib_ArrayBuffer.resi @@ -6,7 +6,7 @@ See: [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Ref /** Type representing an ArrayBuffer object used to represent a generic raw binary data buffer. */ -@notUndefined +@instanceOf("ArrayBuffer") type t /** diff --git a/packages/@rescript/runtime/Stdlib_BigInt64Array.res b/packages/@rescript/runtime/Stdlib_BigInt64Array.res index e0545460c07..adfafe3275f 100644 --- a/packages/@rescript/runtime/Stdlib_BigInt64Array.res +++ b/packages/@rescript/runtime/Stdlib_BigInt64Array.res @@ -1,6 +1,6 @@ /** The `BigInt64Array` typed array represents an array of 64-bit signed integers in platform byte order. See [BigInt64Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("BigInt64Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_BigUint64Array.res b/packages/@rescript/runtime/Stdlib_BigUint64Array.res index 3c8aab184bc..d1df29b60f3 100644 --- a/packages/@rescript/runtime/Stdlib_BigUint64Array.res +++ b/packages/@rescript/runtime/Stdlib_BigUint64Array.res @@ -1,6 +1,6 @@ /** The `BigUint64Array` typed array represents an array of 64-bit unsigned integers in platform byte order. See [BigUint64Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigUint64Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("BigUint64Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Blob.res b/packages/@rescript/runtime/Stdlib_Blob.res index 78df7b6c531..30b2132c82b 100644 --- a/packages/@rescript/runtime/Stdlib_Blob.res +++ b/packages/@rescript/runtime/Stdlib_Blob.res @@ -1,2 +1,3 @@ /** JavaScript Blob API */ +@instanceOf("Blob") type t diff --git a/packages/@rescript/runtime/Stdlib_DataView.res b/packages/@rescript/runtime/Stdlib_DataView.res index e1aae2469f9..b9533aebfe4 100644 --- a/packages/@rescript/runtime/Stdlib_DataView.res +++ b/packages/@rescript/runtime/Stdlib_DataView.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("DataView") type t @new diff --git a/packages/@rescript/runtime/Stdlib_DataView.resi b/packages/@rescript/runtime/Stdlib_DataView.resi index b557557c188..79bf937c87a 100644 --- a/packages/@rescript/runtime/Stdlib_DataView.resi +++ b/packages/@rescript/runtime/Stdlib_DataView.resi @@ -13,7 +13,7 @@ See [`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Referen /** Type representing a DataView. */ -@notUndefined +@instanceOf("DataView") type t /** diff --git a/packages/@rescript/runtime/Stdlib_Date.res b/packages/@rescript/runtime/Stdlib_Date.res index 95216e9a38c..4d77a320fbb 100644 --- a/packages/@rescript/runtime/Stdlib_Date.res +++ b/packages/@rescript/runtime/Stdlib_Date.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Date") type t type msSinceEpoch = float diff --git a/packages/@rescript/runtime/Stdlib_Date.resi b/packages/@rescript/runtime/Stdlib_Date.resi index c0e90397c11..1efa181161b 100644 --- a/packages/@rescript/runtime/Stdlib_Date.resi +++ b/packages/@rescript/runtime/Stdlib_Date.resi @@ -5,7 +5,7 @@ /** A type representing a JavaScript date. */ -@notUndefined +@instanceOf("Date") type t /** diff --git a/packages/@rescript/runtime/Stdlib_File.res b/packages/@rescript/runtime/Stdlib_File.res index 7eaf8e67f77..f600d1809d0 100644 --- a/packages/@rescript/runtime/Stdlib_File.res +++ b/packages/@rescript/runtime/Stdlib_File.res @@ -1,2 +1,3 @@ -/** JavaScript File API */ -type t +/** JavaScript File API. A file is a blob. */ +@instanceOf("File") +type t = private Stdlib_Blob.t diff --git a/packages/@rescript/runtime/Stdlib_Float32Array.res b/packages/@rescript/runtime/Stdlib_Float32Array.res index f916dce88b6..4b87ad539c2 100644 --- a/packages/@rescript/runtime/Stdlib_Float32Array.res +++ b/packages/@rescript/runtime/Stdlib_Float32Array.res @@ -1,6 +1,6 @@ /** The `Float32Array` typed array represents an array of 32-bit floating point numbers in platform byte order. See [Float32Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Float32Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Float64Array.res b/packages/@rescript/runtime/Stdlib_Float64Array.res index fffb73704cb..29cdd62dae6 100644 --- a/packages/@rescript/runtime/Stdlib_Float64Array.res +++ b/packages/@rescript/runtime/Stdlib_Float64Array.res @@ -1,6 +1,6 @@ /** The `Float64Array` typed array represents an array of 64-bit floating point numbers in platform byte order. See [Float64Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Float64Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Int16Array.res b/packages/@rescript/runtime/Stdlib_Int16Array.res index 72aa31ea66b..8a07291437d 100644 --- a/packages/@rescript/runtime/Stdlib_Int16Array.res +++ b/packages/@rescript/runtime/Stdlib_Int16Array.res @@ -1,6 +1,6 @@ /** The `Int16Array` typed array represents an array of twos-complement 16-bit signed integers in platform byte order. See [Int16Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Int16Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Int32Array.res b/packages/@rescript/runtime/Stdlib_Int32Array.res index f537411e29d..70ac9e3089e 100644 --- a/packages/@rescript/runtime/Stdlib_Int32Array.res +++ b/packages/@rescript/runtime/Stdlib_Int32Array.res @@ -1,6 +1,6 @@ /** The `Int32Array` typed array represents an array of twos-complemenet 32-bit signed integers in platform byte order. See [Int32Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Int32Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Int8Array.res b/packages/@rescript/runtime/Stdlib_Int8Array.res index d129824acd2..b03b30369c7 100644 --- a/packages/@rescript/runtime/Stdlib_Int8Array.res +++ b/packages/@rescript/runtime/Stdlib_Int8Array.res @@ -1,6 +1,6 @@ /** The `Int8Array` typed array represents an array of twos-complement 8-bit signed integers. See [Int8Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Int8Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Intl_Collator.res b/packages/@rescript/runtime/Stdlib_Intl_Collator.res index a6d0e4a6d7b..6b3ac1b8b06 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_Collator.res +++ b/packages/@rescript/runtime/Stdlib_Intl_Collator.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Intl.Collator") type t type usage = [#sort | #search] diff --git a/packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res b/packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res index d7904bf02b8..94c8225d3f7 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res +++ b/packages/@rescript/runtime/Stdlib_Intl_DateTimeFormat.res @@ -2,7 +2,7 @@ Bindings to JavaScript's `Intl.DateTimeFormat`. */ -@notUndefined +@instanceOf("Intl.DateTimeFormat") type t type dateStyle = [#full | #long | #medium | #short] diff --git a/packages/@rescript/runtime/Stdlib_Intl_ListFormat.res b/packages/@rescript/runtime/Stdlib_Intl_ListFormat.res index 86715d40121..03b4e45c584 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_ListFormat.res +++ b/packages/@rescript/runtime/Stdlib_Intl_ListFormat.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Intl.ListFormat") type t type listType = [ diff --git a/packages/@rescript/runtime/Stdlib_Intl_Locale.res b/packages/@rescript/runtime/Stdlib_Intl_Locale.res index 0e4bf97f75d..d5b37053076 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_Locale.res +++ b/packages/@rescript/runtime/Stdlib_Intl_Locale.res @@ -2,7 +2,7 @@ Bindings to JavaScript's `Intl.Locale`. */ -@notUndefined +@instanceOf("Intl.Locale") type t type options = { diff --git a/packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res b/packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res index bd9d53da77b..a26a3f8d128 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res +++ b/packages/@rescript/runtime/Stdlib_Intl_NumberFormat.res @@ -1,6 +1,6 @@ module Grouping = Stdlib_Intl_NumberFormat_Grouping -@notUndefined +@instanceOf("Intl.NumberFormat") type t /** diff --git a/packages/@rescript/runtime/Stdlib_Intl_PluralRules.res b/packages/@rescript/runtime/Stdlib_Intl_PluralRules.res index c94c534c76a..688ad7f1b2b 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_PluralRules.res +++ b/packages/@rescript/runtime/Stdlib_Intl_PluralRules.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Intl.PluralRules") type t type localeType = [#cardinal | #ordinal] diff --git a/packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res b/packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res index 37fa2f5ae90..5d1da8af913 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res +++ b/packages/@rescript/runtime/Stdlib_Intl_RelativeTimeFormat.res @@ -3,7 +3,7 @@ Bindings to JavaScript's `Intl.RelativeTimeFormat`. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) for API details. */ -@notUndefined +@instanceOf("Intl.RelativeTimeFormat") type t type numeric = [#always | #auto] diff --git a/packages/@rescript/runtime/Stdlib_Intl_Segmenter.res b/packages/@rescript/runtime/Stdlib_Intl_Segmenter.res index ff00a53c644..63a14731fe0 100644 --- a/packages/@rescript/runtime/Stdlib_Intl_Segmenter.res +++ b/packages/@rescript/runtime/Stdlib_Intl_Segmenter.res @@ -3,7 +3,7 @@ Bindings to JavaScript's `Intl.Segmenter`. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) for API details. */ -@notUndefined +@instanceOf("Intl.Segmenter") type t type granularity = [#grapheme | #word | #sentence] diff --git a/packages/@rescript/runtime/Stdlib_JsError.res b/packages/@rescript/runtime/Stdlib_JsError.res index 06229731789..d80274e282f 100644 --- a/packages/@rescript/runtime/Stdlib_JsError.res +++ b/packages/@rescript/runtime/Stdlib_JsError.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Error") type t @get external stack: t => option = "stack" diff --git a/packages/@rescript/runtime/Stdlib_JsError.resi b/packages/@rescript/runtime/Stdlib_JsError.resi index 854d19c5d73..1580f3ac784 100644 --- a/packages/@rescript/runtime/Stdlib_JsError.resi +++ b/packages/@rescript/runtime/Stdlib_JsError.resi @@ -5,7 +5,7 @@ See [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ */ /** Represents a JavaScript error. */ -@notUndefined +@instanceOf("Error") type t /** diff --git a/packages/@rescript/runtime/Stdlib_Map.res b/packages/@rescript/runtime/Stdlib_Map.res index 706132cc007..b7321c2249f 100644 --- a/packages/@rescript/runtime/Stdlib_Map.res +++ b/packages/@rescript/runtime/Stdlib_Map.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Map") type t<'k, 'v> external asIterable: t<'k, 'v> => Stdlib_Iterable.t<('k, 'v)> = "%identity" diff --git a/packages/@rescript/runtime/Stdlib_Map.resi b/packages/@rescript/runtime/Stdlib_Map.resi index 28975651ee1..54ff66bc4f9 100644 --- a/packages/@rescript/runtime/Stdlib_Map.resi +++ b/packages/@rescript/runtime/Stdlib_Map.resi @@ -7,7 +7,7 @@ See [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Gl /** Type representing an instance of `Map`. */ -@notUndefined +@instanceOf("Map") type t<'k, 'v> /** diff --git a/packages/@rescript/runtime/Stdlib_RegExp.res b/packages/@rescript/runtime/Stdlib_RegExp.res index 9f1b5736a20..3f5c9269a6d 100644 --- a/packages/@rescript/runtime/Stdlib_RegExp.res +++ b/packages/@rescript/runtime/Stdlib_RegExp.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("RegExp") type t module Result = { diff --git a/packages/@rescript/runtime/Stdlib_RegExp.resi b/packages/@rescript/runtime/Stdlib_RegExp.resi index f6822678973..0d5b95e2f36 100644 --- a/packages/@rescript/runtime/Stdlib_RegExp.resi +++ b/packages/@rescript/runtime/Stdlib_RegExp.resi @@ -7,7 +7,7 @@ See [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference /** Type representing an instantiated `RegExp`. */ -@notUndefined +@instanceOf("RegExp") type t module Result: { diff --git a/packages/@rescript/runtime/Stdlib_Set.res b/packages/@rescript/runtime/Stdlib_Set.res index 3a2beee3ef3..968900ae366 100644 --- a/packages/@rescript/runtime/Stdlib_Set.res +++ b/packages/@rescript/runtime/Stdlib_Set.res @@ -1,4 +1,4 @@ -@notUndefined +@instanceOf("Set") type t<'a> external asIterable: t<'a> => Stdlib_Iterable.t<'a> = "%identity" diff --git a/packages/@rescript/runtime/Stdlib_Set.resi b/packages/@rescript/runtime/Stdlib_Set.resi index 8d32617608f..2a6967ef705 100644 --- a/packages/@rescript/runtime/Stdlib_Set.resi +++ b/packages/@rescript/runtime/Stdlib_Set.resi @@ -7,7 +7,7 @@ See [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Gl /** Type representing an instance of `Set`. */ -@notUndefined +@instanceOf("Set") type t<'a> /** diff --git a/packages/@rescript/runtime/Stdlib_Symbol.res b/packages/@rescript/runtime/Stdlib_Symbol.res index bbdb9783135..af37431e7fe 100644 --- a/packages/@rescript/runtime/Stdlib_Symbol.res +++ b/packages/@rescript/runtime/Stdlib_Symbol.res @@ -1,4 +1,4 @@ -@notUndefined +@typeOf("symbol") type t @val external make: string => t = "Symbol" diff --git a/packages/@rescript/runtime/Stdlib_Symbol.resi b/packages/@rescript/runtime/Stdlib_Symbol.resi index b9810319639..509fd90c6f5 100644 --- a/packages/@rescript/runtime/Stdlib_Symbol.resi +++ b/packages/@rescript/runtime/Stdlib_Symbol.resi @@ -7,7 +7,7 @@ Compiles to a regular JavaScript Symbol. /** Type representing a Symbol. */ -@notUndefined +@typeOf("symbol") type t /** diff --git a/packages/@rescript/runtime/Stdlib_Uint16Array.res b/packages/@rescript/runtime/Stdlib_Uint16Array.res index 7f0dcbf8441..feb4eae0c69 100644 --- a/packages/@rescript/runtime/Stdlib_Uint16Array.res +++ b/packages/@rescript/runtime/Stdlib_Uint16Array.res @@ -1,6 +1,6 @@ /** The `Uint16Array` typed array represents an array of 16-bit unsigned integers in platform byte order. See [Uint16Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Uint16Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Uint32Array.res b/packages/@rescript/runtime/Stdlib_Uint32Array.res index b18795a8b4f..41ca7c40ae9 100644 --- a/packages/@rescript/runtime/Stdlib_Uint32Array.res +++ b/packages/@rescript/runtime/Stdlib_Uint32Array.res @@ -1,6 +1,6 @@ /** The `Uint32Array` typed array represents an array of 32-bit unsigned integers in platform byte order. See [Uint32Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Uint32Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Uint8Array.res b/packages/@rescript/runtime/Stdlib_Uint8Array.res index 4866d45956b..66964e5a527 100644 --- a/packages/@rescript/runtime/Stdlib_Uint8Array.res +++ b/packages/@rescript/runtime/Stdlib_Uint8Array.res @@ -1,6 +1,6 @@ /** The `Uint8Array` typed array represents an array of 8-bit unsigned integers. See [Uint8Array on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Uint8Array") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res b/packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res index 769267c019b..79959c9d05c 100644 --- a/packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res +++ b/packages/@rescript/runtime/Stdlib_Uint8ClampedArray.res @@ -1,6 +1,6 @@ /** The `Uint8ClampedArray` typed array represents an array of 8-bit unsigned integers clamped to 0-255. See [Uint8ClampedArray on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray) */ -@editor.completeFrom(Stdlib.TypedArray) +@editor.completeFrom(Stdlib.TypedArray) @instanceOf("Uint8ClampedArray") type t = Stdlib_TypedArray.t module Constants = { diff --git a/packages/@rescript/runtime/Stdlib_WeakMap.res b/packages/@rescript/runtime/Stdlib_WeakMap.res index 4ac50a49301..7ad60562d51 100644 --- a/packages/@rescript/runtime/Stdlib_WeakMap.res +++ b/packages/@rescript/runtime/Stdlib_WeakMap.res @@ -5,7 +5,7 @@ Weak maps keep key/value pairs where keys must be objects and the references do */ /** Mutable weak map storing values of type `'v` with object keys `'k`. */ -@notUndefined +@instanceOf("WeakMap") type t<'k, 'v> /** diff --git a/packages/@rescript/runtime/Stdlib_WeakSet.res b/packages/@rescript/runtime/Stdlib_WeakSet.res index 51ff206270f..705ec6ae0f3 100644 --- a/packages/@rescript/runtime/Stdlib_WeakSet.res +++ b/packages/@rescript/runtime/Stdlib_WeakSet.res @@ -5,7 +5,7 @@ Weak sets store references to objects without preventing those objects from bein */ /** Mutable weak set storing object references of type `'a`. */ -@notUndefined +@instanceOf("WeakSet") type t<'a> /** diff --git a/tests/tests/src/UntaggedVariants.mjs b/tests/tests/src/UntaggedVariants.mjs index a9d410ad5ab..df8f7b89be7 100644 --- a/tests/tests/src/UntaggedVariants.mjs +++ b/tests/tests/src/UntaggedVariants.mjs @@ -517,7 +517,7 @@ async function classifyAll(t) { console.log(t.name); return; } - if (t instanceof Blob) { + if (t instanceof Blob && !(t instanceof File)) { console.log(t.size); return; } diff --git a/tests/tests/src/untagged_file_blob_test.mjs b/tests/tests/src/untagged_file_blob_test.mjs new file mode 100644 index 00000000000..6fb2f5a1c5d --- /dev/null +++ b/tests/tests/src/untagged_file_blob_test.mjs @@ -0,0 +1,41 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function kind(upload) { + if (upload instanceof Blob && !(upload instanceof File)) { + return "blob"; + } else { + return "file"; + } +} + +function onlyBlob(upload) { + if (upload instanceof Blob && !(upload instanceof File)) { + return "blob"; + } else { + return "other"; + } +} + +Mocha.describe("Untagged_file_blob_test", () => { + Mocha.test("a file is not taken for a blob", () => { + Test_utils.eq("File \"untagged_file_blob_test.res\", line 25, characters 7-14", "blob", kind(new Blob(["x"]))); + Test_utils.eq("File \"untagged_file_blob_test.res\", line 26, characters 7-14", "file", kind(new File(["x"], "a.txt"))); + }); + Mocha.test("a blob case alone excludes files", () => { + Test_utils.eq("File \"untagged_file_blob_test.res\", line 30, characters 7-14", "blob", onlyBlob(new Blob(["x"]))); + Test_utils.eq("File \"untagged_file_blob_test.res\", line 31, characters 7-14", "other", onlyBlob(new File(["x"], "a.txt"))); + }); + Mocha.test("a file upcasts to a blob", () => { + let blob = new File(["x"], "a.txt"); + Test_utils.eq("File \"untagged_file_blob_test.res\", line 36, characters 7-14", "file", kind(blob)); + }); +}); + +export { + kind, + onlyBlob, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_file_blob_test.res b/tests/tests/src/untagged_file_blob_test.res new file mode 100644 index 00000000000..62ec91c6d2f --- /dev/null +++ b/tests/tests/src/untagged_file_blob_test.res @@ -0,0 +1,38 @@ +open Mocha +open Test_utils + +@new external makeBlob: array => Blob.t = "Blob" +@new external makeFile: (array, string) => File.t = "File" + +// Blob is declared before its subclass File (#8938) +@unboxed +type upload = Blob(Blob.t) | File(File.t) + +let kind = upload => + switch upload { + | Blob(_) => "blob" + | File(_) => "file" + } + +let onlyBlob = upload => + switch upload { + | Blob(_) => "blob" + | _ => "other" + } + +describe(__MODULE__, () => { + test("a file is not taken for a blob", () => { + eq(__LOC__, "blob", kind(Blob(makeBlob(["x"])))) + eq(__LOC__, "file", kind(File(makeFile(["x"], "a.txt")))) + }) + + test("a blob case alone excludes files", () => { + eq(__LOC__, "blob", onlyBlob(Blob(makeBlob(["x"])))) + eq(__LOC__, "other", onlyBlob(File(makeFile(["x"], "a.txt")))) + }) + + test("a file upcasts to a blob", () => { + let blob: Blob.t = (makeFile(["x"], "a.txt") :> Blob.t) + eq(__LOC__, "file", kind(Obj.magic(blob))) + }) +}) From 9c6de8b94b73ce8adff70e65fcc1920b0e2f5f30 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 16:53:58 +0000 Subject: [PATCH 06/11] Document the stdlib's runtime annotations in the untagged variants spec Why: With the stdlib annotated, `Blob.t` and `File.t` are never undefined and #8938 is fixed, which the spec and its test still stated the other way round. Rule 5 lists the stdlib's annotations, rule 12 unboxes `option` of a variant with a `Blob.t` payload, and the known bug of overlapping classes is gone. The typed-array bug stays, filed as #8955. JS modified (option of a variant with a Blob payload): Example: @unboxed type withBlob = BBlob(Blob.t) | BString(string) let someWithBlob = (v: withBlob): option => Some(v) Before (js): return Primitive_option.some(v); After (js): return v; Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/UntaggedVariants.md | 48 +++++++++++-------- .../tests/src/untagged_variants_spec_test.mjs | 4 +- .../tests/src/untagged_variants_spec_test.res | 2 +- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 8c7cd9df895..3e7361bac2e 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -170,8 +170,21 @@ through `globalThis` (#8785). The compiler trusts the annotations, as it trusts the type of an external: a value that is not what its type declares is recognized by what it is (rule 10). +The stdlib annotates the types of the classes it binds: + +| Type | Annotation | +| --- | --- | +| `Date.t`, `RegExp.t`, `ArrayBuffer.t`, `DataView.t`, `Map.t`, `Set.t`, `WeakMap.t`, `WeakSet.t`, `Blob.t` | `@instanceOf` the class of the same name | +| `File.t`, a private abbreviation of `Blob.t` | `@instanceOf("File")`, a subclass of `Blob` | +| `JsError.t` | `@instanceOf("Error")` | +| `Intl.Collator.t`, `Intl.DateTimeFormat.t`, `Intl.ListFormat.t`, `Intl.Locale.t`, `Intl.NumberFormat.t`, `Intl.PluralRules.t`, `Intl.RelativeTimeFormat.t`, `Intl.Segmenter.t` | `@instanceOf("Intl.Collator")` and so on | +| `Int8Array.t` and the other typed arrays, public abbreviations of `TypedArray.t<_>` | `@instanceOf` the class of the same name | +| `Symbol.t` | `@typeOf("symbol")` | + Types without annotations are recognized from a fixed list of type names in -`ast_untagged_variants.ml:type_to_instanceof_backed_obj`. The predefined +`ast_untagged_variants.ml:type_to_instanceof_backed_obj`. Every listed stdlib +type is annotated, so the list only covers other types printed with one of +these paths. The predefined `promise<_>` and `array<_>` are matched by path; the others by the printed path of the type: @@ -184,8 +197,8 @@ path of the type: | `Int8Array.t`, `Uint8Array.t`, `Uint8ClampedArray.t`, `Int16Array.t`, `Uint16Array.t`, `Int32Array.t`, `Uint32Array.t`, `Float32Array.t`, `Float64Array.t`, `BigInt64Array.t`, `BigUint64Array.t` | the class of the same name | The listed classes have no superclasses. The typed-array types are -abbreviations of one type, `TypedArray.t<_>`, and are told apart only by the -name as written. +abbreviations of one type, `TypedArray.t<_>`, and are told apart only by +their annotations, which are read on the name as written (Known bugs). - Implementation: `variant_layout.ml:runtime_annotations`, `declared_block_type`, `check_manifest_annotation`; @@ -195,9 +208,10 @@ name as written. `variant_runtime.ml:Instance`; `js_exp_make.ml:class_constructor`. - Tests: `spec_test` rules 4 and 5; `tests/tests/src/untagged_runtime_annotations_test.res`, - `untagged_runtime_annotations_shadow_test.res`; the fixtures above, + `untagged_runtime_annotations_shadow_test.res`, + `untagged_file_blob_test.res`; the fixtures above, `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res`. -- Shaped by #6296, #6383, #6396, #7788, #8009, #8950. Related: #7147. +- Shaped by #6296, #6383, #6396, #7788, #8009, #8950, #8948. Related: #7147. ### 6. Well-formedness @@ -387,16 +401,17 @@ An `@as(null)` case does not prevent unboxing; `Some(N)` is `null`. | `@as(null) WNull \| WString(string)` | `v` | | `@as(undefined) WUndefined \| WUString(string)` | `Primitive_option.some(v)` | | `@as("n") ONone \| OPaque(opaque)` | `Primitive_option.some(v)` | -| `BBlob(Blob.t) \| BString(string)` | `Primitive_option.some(v)` | +| `BBlob(Blob.t) \| BString(string)` | `v` | | `DDate(Date.t) \| DString(string)` | `v` | | `RRecord(record) \| RString(string)` with `type record = {y: option}` | `v` | | `INone \| IRecord({x: option})` | `Primitive_option.some(v)` | | `@as("n") UONone \| UOption(unboxedOption)` with `@unboxed type unboxedOption = {x: option}` | `Primitive_option.some(v)` | | `@as("n") USNone \| UString(unboxedString)` with `@unboxed type unboxedString = {s: string}` | `v` | -The stdlib marks `Date.t`, `RegExp.t`, `Map.t`, `Set.t`, `WeakMap.t`, -`WeakSet.t`, `ArrayBuffer.t`, `DataView.t`, the typed arrays and other -abstract types `@notUndefined`; `Blob.t` and `File.t` are not marked. +The stdlib's annotated types (rule 5) cannot hold `undefined`. It marks the +JSX element and component types, `Iterator.t`, `AsyncIterator.t`, +`TypedArray.t`, `Intl.Segments.t`, `Intl.NumberFormat.Grouping.t` and +`Lazy.t` with `@notUndefined`. Rule 5 keeps an annotated abbreviation from claiming this of a type that can hold `undefined`. @@ -474,18 +489,9 @@ Console.log2(x == None, mk(U) == None) // true false `Const_constructor`, including one whose literal is `undefined`; that this is the cause is conjectured, not established. -**Overlapping instance classes follow declaration order** (#8938). - -```rescript -@unboxed type b = B(Blob.t) | F(File.t) -let f = v => switch v { | B(_) => "blob" | F(_) => "file" } -Console.log(f(F(%raw(`new File(["x"], "a.txt")`)))) // blob -``` - -A `File` is a `Blob`, and the `Blob` check runs first. - -**Typed arrays are classified by the name as written.** The typed-array types -are one type, so a value of one can be wrapped in the case of another. +**Typed arrays share one type** (#8955). The typed-array types are public +abbreviations of `TypedArray.t<_>`, each annotated with its class, so a value +of one can be wrapped in the case of another. ```rescript @unboxed type ta = I8(Int8Array.t) | U8(Uint8Array.t) diff --git a/tests/tests/src/untagged_variants_spec_test.mjs b/tests/tests/src/untagged_variants_spec_test.mjs index fe9506bae47..bc14ce120c2 100644 --- a/tests/tests/src/untagged_variants_spec_test.mjs +++ b/tests/tests/src/untagged_variants_spec_test.mjs @@ -199,7 +199,7 @@ function someWithVariable(v) { } function someWithBlob(v) { - return Primitive_option.some(v); + return v; } function someWithDate(v) { @@ -358,7 +358,7 @@ Mocha.describe("Untagged_variants_spec_test", () => { Test_utils.eq("File \"untagged_variants_spec_test.res\", line 326, characters 7-14", someWithUndefined("a"), "a"); Test_utils.eq("File \"untagged_variants_spec_test.res\", line 329, characters 7-14", someWithOpaque(undefined) === undefined, false); Test_utils.eq("File \"untagged_variants_spec_test.res\", line 330, characters 7-14", someWithVariable(undefined) === undefined, false); - Test_utils.eq("File \"untagged_variants_spec_test.res\", line 331, characters 7-14", someWithBlob(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 331, characters 7-14", someWithBlob(undefined) === undefined, true); Test_utils.eq("File \"untagged_variants_spec_test.res\", line 332, characters 7-14", someWithDate(undefined) === undefined, true); Test_utils.eq("File \"untagged_variants_spec_test.res\", line 333, characters 7-14", someWithRecord(undefined) === undefined, true); Test_utils.eq("File \"untagged_variants_spec_test.res\", line 334, characters 7-14", someWithInlineRecord(undefined) === undefined, false); diff --git a/tests/tests/src/untagged_variants_spec_test.res b/tests/tests/src/untagged_variants_spec_test.res index 9976b193c9b..9f7bec046c9 100644 --- a/tests/tests/src/untagged_variants_spec_test.res +++ b/tests/tests/src/untagged_variants_spec_test.res @@ -328,7 +328,7 @@ describe(__MODULE__, () => { // distinct from `None`, an unboxed one does not. eq(__LOC__, someWithOpaque(OPaque(raw(Nullable.undefined))) == None, false) eq(__LOC__, someWithVariable(Var(raw(Nullable.undefined))) == None, false) - eq(__LOC__, someWithBlob(BBlob(raw(Nullable.undefined))) == None, false) + eq(__LOC__, someWithBlob(BBlob(raw(Nullable.undefined))) == None, true) eq(__LOC__, someWithDate(DDate(raw(Nullable.undefined))) == None, true) eq(__LOC__, someWithRecord(RRecord(raw(Nullable.undefined))) == None, true) eq(__LOC__, someWithInlineRecord(raw(Nullable.undefined)) == None, false) From 31cb314a724f6a9f65da8e8767612493233b8888 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 21 Oct 2026 18:04:48 +0000 Subject: [PATCH 07/11] Recognize untagged payloads only by annotations and builtin types Why: With the stdlib annotated, the classifier's list of stdlib type names only matched types that happen to be printed like a stdlib type, and its name-based dict check took any type named `dict` for an object. The list, matched before and after expansion, is gone, and so is the dict check: a payload is recognized by its `@instanceOf` or `@typeOf`, or by the builtin type it expands to. Generated code is unchanged for the stdlib's types. Diagnostic added (a local type named dict): Example: type dict<'a> @unboxed type t = D(dict) | S(string) Before (js): if (typeof v === "object") { return "D"; } else { return "S"; } After (diagnostic): This untagged variant definition is invalid: Case D has a payload that is not of one of the recognized shapes (object, array, etc). Then it must be the only case with payloads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- .../untagged-no-hardcoded-classes.internal.md | 1 + compiler/ml/ast_untagged_variants.ml | 41 ---------------- compiler/ml/typeopt.ml | 7 +-- compiler/ml/variant_layout.ml | 48 ++++++++----------- 4 files changed, 23 insertions(+), 74 deletions(-) create mode 100644 changes/untagged-no-hardcoded-classes.internal.md diff --git a/changes/untagged-no-hardcoded-classes.internal.md b/changes/untagged-no-hardcoded-classes.internal.md new file mode 100644 index 00000000000..73ab3199df6 --- /dev/null +++ b/changes/untagged-no-hardcoded-classes.internal.md @@ -0,0 +1 @@ +- Untagged variants recognize stdlib classes only through their `@instanceOf` annotations; the compiler's list of stdlib type names is gone. diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index 67b3076ada1..0a7827d8bc4 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -358,47 +358,6 @@ let check_runtime_annotation ~loc ~is_abstract_or_abbreviation RuntimeAnnotationOnRepresentedType (runtime_attribute_name annotation) )) -let type_is_builtin_object (t : Types.type_expr) = - match t.desc with - | Tconstr (Path.Pident ident, [_], _) when Ident.name ident = "dict" -> true - | Tconstr (path, _, _) -> - let name = Path.name path in - name = "Stdlib.Dict.t" || name = "Stdlib_Dict.t" - | _ -> false - -let type_to_instanceof_backed_obj (t : Types.type_expr) = - let cls name = Some (Instance.Class {name; ancestors = []}) in - match t.desc with - | Tconstr (path, _, _) when Path.same path Predef.path_promise -> - cls "Promise" - | Tconstr (path, _, _) when Path.same path Predef.path_array -> - Some Instance.Array - | Tconstr (path, _, _) -> ( - match Path.name path with - | "Stdlib_ArrayBuffer.t" -> cls "ArrayBuffer" - | "Stdlib.BigInt64Array.t" -> cls "BigInt64Array" - | "Stdlib.BigUint64Array.t" -> cls "BigUint64Array" - | "Stdlib.DataView.t" -> cls "DataView" - | "Stdlib_Date.t" -> cls "Date" - | "Stdlib.Float32Array.t" -> cls "Float32Array" - | "Stdlib.Float64Array.t" -> cls "Float64Array" - | "Stdlib.Int16Array.t" -> cls "Int16Array" - | "Stdlib.Int32Array.t" -> cls "Int32Array" - | "Stdlib.Int8Array.t" -> cls "Int8Array" - | "Stdlib_RegExp.t" -> cls "RegExp" - | "Stdlib.Uint16Array.t" -> cls "Uint16Array" - | "Stdlib.Uint32Array.t" -> cls "Uint32Array" - | "Stdlib.Uint8Array.t" -> cls "Uint8Array" - | "Stdlib.Uint8ClampedArray.t" -> cls "Uint8ClampedArray" - | "Stdlib_File.t" -> cls "File" - | "Stdlib_Blob.t" -> cls "Blob" - | "Stdlib.Set.t" -> cls "Set" - | "Stdlib.Map.t" -> cls "Map" - | "Stdlib.WeakSet.t" -> cls "WeakSet" - | "Stdlib.WeakMap.t" -> cls "WeakMap" - | _ -> None) - | _ -> None - let process_tag_name (attrs : Parsetree.attributes) = let st = ref None in Ext_list.iter attrs (fun ({txt; loc}, payload) -> diff --git a/compiler/ml/typeopt.ml b/compiler/ml/typeopt.ml index b0bec6b2900..d20ab709324 100644 --- a/compiler/ml/typeopt.ml +++ b/compiler/ml/typeopt.ml @@ -96,15 +96,12 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) = && ((not payload_is_unboxed) || match cd.cd_args with - | Cstr_tuple [t] -> - Ast_untagged_variants.type_is_builtin_object t - || type_cannot_contain_undefined t env + | Cstr_tuple [t] -> type_cannot_contain_undefined t env | Cstr_tuple [] -> true | Cstr_tuple (_ :: _ :: _) -> true (* Not actually possible for an unboxed payload. *) | Cstr_record [{ld_type = t}] -> - Ast_untagged_variants.type_is_builtin_object t - || type_cannot_contain_undefined t env + type_cannot_contain_undefined t env | Cstr_record ([] | _ :: _ :: _) -> true) && all_cases_cannot_contain_undefined (position + 1) rest in diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index bfa6976b9aa..75652afd396 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -128,34 +128,26 @@ let get_block_type_from_typ ~env (t : Types.type_expr) : block_type option = match declared_block_type ~env t with | Some _ as declared -> declared | None -> ( - (* First check the original (unexpanded) type for typed arrays and other instance types *) - match type_to_instanceof_backed_obj t with - | Some instance_type -> Some (InstanceType instance_type) - | None -> ( - (* If original type didn't match, expand and try standard checks *) - let expanded_t = Ctype.expand_head env t in - match expanded_t with - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_string -> - Some StringType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_int -> - Some IntType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_float -> - Some FloatType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bigint -> - Some BigintType - | {desc = Tconstr (path, _, _)} when Path.same path Predef.path_bool -> - Some BooleanType - | {desc = Tarrow _} -> Some FunctionType - | {desc = Tconstr _} as expanded_t when type_is_builtin_object expanded_t - -> - Some ObjectType - | {desc = Tconstr _} as expanded_t - when type_to_instanceof_backed_obj expanded_t |> Option.is_some -> ( - match type_to_instanceof_backed_obj expanded_t with - | None -> None - | Some instance_type -> Some (InstanceType instance_type)) - | {desc = Ttuple _} -> Some (InstanceType Array) - | _ -> None)) + (* Builtin types have no declaration to annotate *) + match (Ctype.expand_head env t).desc with + | Tconstr (path, _, _) when Path.same path Predef.path_string -> + Some StringType + | Tconstr (path, _, _) when Path.same path Predef.path_int -> Some IntType + | Tconstr (path, _, _) when Path.same path Predef.path_float -> + Some FloatType + | Tconstr (path, _, _) when Path.same path Predef.path_bigint -> + Some BigintType + | Tconstr (path, _, _) when Path.same path Predef.path_bool -> + Some BooleanType + | Tconstr (path, _, _) when Path.same path Predef.path_dict -> + Some ObjectType + | Tconstr (path, _, _) when Path.same path Predef.path_array -> + Some (InstanceType Array) + | Tconstr (path, _, _) when Path.same path Predef.path_promise -> + Some (InstanceType (Class {name = "Promise"; ancestors = []})) + | Ttuple _ -> Some (InstanceType Array) + | Tarrow _ -> Some FunctionType + | _ -> None) let get_block_type ~env (cstr : Types.constructor_declaration) : block_type = match cstr.cd_args with From dae5d2025045decbe82561f048a20554a0f9c473 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 21 Oct 2026 18:36:58 +0000 Subject: [PATCH 08/11] Test that payloads are not recognized by type name Why: The classifier used to recognize payloads by the printed path of their type; nothing pinned that it no longer does. An unannotated local `Stdlib_Date.t`, which the removed list would have taken for a date, is an unknown payload, and aliases of builtin types are recognized through their expansion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- ...agged_unannotated_stdlib_name.res.expected | 10 ++++++ .../untagged_unannotated_stdlib_name.res | 8 +++++ .../tests/src/untagged_builtin_alias_test.mjs | 31 +++++++++++++++++++ .../tests/src/untagged_builtin_alias_test.res | 25 +++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/untagged_unannotated_stdlib_name.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/untagged_unannotated_stdlib_name.res create mode 100644 tests/tests/src/untagged_builtin_alias_test.mjs create mode 100644 tests/tests/src/untagged_builtin_alias_test.res diff --git a/tests/build_tests/super_errors/expected/untagged_unannotated_stdlib_name.res.expected b/tests/build_tests/super_errors/expected/untagged_unannotated_stdlib_name.res.expected new file mode 100644 index 00000000000..e9ca2b50ef2 --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_unannotated_stdlib_name.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/untagged_unannotated_stdlib_name.res:8:10-28 + + 6 │ + 7 │ @unboxed + 8 │ type t = Date(Stdlib_Date.t) | Text(string) + 9 │ + + This untagged variant definition is invalid: Case Date has a payload that is not of one of the recognized shapes (object, array, etc). Then it must be the only case with payloads. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/untagged_unannotated_stdlib_name.res b/tests/build_tests/super_errors/fixtures/untagged_unannotated_stdlib_name.res new file mode 100644 index 00000000000..fc12725effe --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_unannotated_stdlib_name.res @@ -0,0 +1,8 @@ +// Payloads are not recognized by their type's name: this unannotated type +// only shares its path with a stdlib class +module Stdlib_Date = { + type t +} + +@unboxed +type t = Date(Stdlib_Date.t) | Text(string) diff --git a/tests/tests/src/untagged_builtin_alias_test.mjs b/tests/tests/src/untagged_builtin_alias_test.mjs new file mode 100644 index 00000000000..fbaf95ceb5b --- /dev/null +++ b/tests/tests/src/untagged_builtin_alias_test.mjs @@ -0,0 +1,31 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function kind(value) { + if (Array.isArray(value)) { + return "pair"; + } + switch (typeof value) { + case "object" : + return "entries"; + case "string" : + return value; + } +} + +Mocha.describe("Untagged_builtin_alias_test", () => { + Mocha.test("aliases of builtin types", () => { + Test_utils.eq("File \"untagged_builtin_alias_test.res\", line 21, characters 7-14", "entries", kind({ + a: 1 + })); + Test_utils.eq("File \"untagged_builtin_alias_test.res\", line 22, characters 7-14", "text", "text"); + Test_utils.eq("File \"untagged_builtin_alias_test.res\", line 23, characters 7-14", "pair", "pair"); + }); +}); + +export { + kind, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_builtin_alias_test.res b/tests/tests/src/untagged_builtin_alias_test.res new file mode 100644 index 00000000000..d5de5e80ee2 --- /dev/null +++ b/tests/tests/src/untagged_builtin_alias_test.res @@ -0,0 +1,25 @@ +open Mocha +open Test_utils + +// Aliases of builtin types are recognized through their expansion +type entries = dict +type label = string +type pair = (int, int) + +@unboxed +type value = Entries(entries) | Label(label) | Pair(pair) + +let kind = value => + switch value { + | Entries(_) => "entries" + | Label(label) => label + | Pair(_) => "pair" + } + +describe(__MODULE__, () => { + test("aliases of builtin types", () => { + eq(__LOC__, "entries", kind(Entries(dict{"a": 1}))) + eq(__LOC__, "text", kind(Label("text"))) + eq(__LOC__, "pair", kind(Pair((1, 2)))) + }) +}) From 735bb93d0ba2bc6eee07cbd931be679328417627 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 16:54:36 +0000 Subject: [PATCH 09/11] Drop the name list from the untagged variants spec Why: Payloads are now recognized only by annotations and builtin types, so the spec's list of stdlib type names and the fallback to it are gone. Rule 4 lists `promise<_>` as the one builtin class and says that a type is never recognized by its name; rule 5 keeps the stdlib's annotations. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/UntaggedVariants.md | 51 ++++++++++++--------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 3e7361bac2e..46143d4a542 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -90,29 +90,31 @@ into one kind: | a function type | function | `typeof x === "function"` | | a record (not `@unboxed`), an inline record, `dict<_>` | object | `typeof x === "object"` | | `array<_>`, a tuple | instance `Array` | `Array.isArray(x)` | -| a type declared with `@instanceOf("C")`, or listed in rule 5 | instance `C` | `x instanceof C` | +| a type declared with `@instanceOf("C")` (rule 5) | instance `C` | `x instanceof C` | +| `promise<_>` | instance `Promise` | `x instanceof Promise` | | a type declared with `@typeOf("k")` | kind `k` | `typeof x === "k"` | | anything else: an abstract type, a type variable, a variant, `list<_>`, an `@unboxed` record | unknown | no check | The annotations of rule 5 are read first, on the type as written and on each -type it abbreviates. Then the list of rule 5 is consulted on the type as -written, and then the payload type is classified after expanding -abbreviations (`type myDate = Date.t` is an instance case). `@typeOf("number")` +type it abbreviates. Otherwise the payload type is classified after expanding +abbreviations: `type ints = array` is an array case, and `type name = +string` a string case. A type is never recognized by its name. `@typeOf("number")` is the number kind, and `@typeOf("symbol")` is a kind of its own, with no builtin payload type. `@notUndefined` on an abstract type leaves it unknown. - Implementation: `variant_layout.ml:get_block_type_from_typ`, - `get_block_type`, `declared_block_type`; - `ast_untagged_variants.ml:type_is_builtin_object`. + `get_block_type`, `declared_block_type`. - Tests: `spec_test` rules 4 (`kinds`, `tuples`, `aliasDict`, `instances`, `unknown`); fixtures `UntaggedConstructorMoreThanOneArg.res`, `UntaggedConstructorAttributeMoreThanOneArg.res`, `UntaggedNonUnary1.res`, `UntaggedNonUnary2.res` ("Constructor A has more than one argument."), - `UntaggedUnknown.res` (`list` is unknown); - `tests/tests/src/untagged_runtime_annotations_test.res`. + `UntaggedUnknown.res` (`list` is unknown), + `untagged_unannotated_stdlib_name.res` (a type named like a stdlib class + is unknown); `tests/tests/src/untagged_runtime_annotations_test.res`, + `untagged_builtin_alias_test.res`. - Shaped by #6121, #6208, #6219, #6279, #6368, #6394, #6550, #6670. -### 5. Declared and listed instance types +### 5. Declared instance types `@instanceOf("C")` on a type declares that its values are instances of the global class at the path `C`, such as `"Date"` or `"Intl.Locale"`. @@ -181,37 +183,22 @@ The stdlib annotates the types of the classes it binds: | `Int8Array.t` and the other typed arrays, public abbreviations of `TypedArray.t<_>` | `@instanceOf` the class of the same name | | `Symbol.t` | `@typeOf("symbol")` | -Types without annotations are recognized from a fixed list of type names in -`ast_untagged_variants.ml:type_to_instanceof_backed_obj`. Every listed stdlib -type is annotated, so the list only covers other types printed with one of -these paths. The predefined -`promise<_>` and `array<_>` are matched by path; the others by the printed -path of the type: - -| Type | Class | -| --- | --- | -| `promise<_>` | `Promise` | -| `array<_>` | `Array` (checked with `Array.isArray`) | -| `Date.t`, `RegExp.t`, `File.t`, `Blob.t`, `ArrayBuffer.t`, `DataView.t` | the class of the same name | -| `Set.t`, `Map.t`, `WeakSet.t`, `WeakMap.t` | the class of the same name | -| `Int8Array.t`, `Uint8Array.t`, `Uint8ClampedArray.t`, `Int16Array.t`, `Uint16Array.t`, `Int32Array.t`, `Uint32Array.t`, `Float32Array.t`, `Float64Array.t`, `BigInt64Array.t`, `BigUint64Array.t` | the class of the same name | - -The listed classes have no superclasses. The typed-array types are -abbreviations of one type, `TypedArray.t<_>`, and are told apart only by -their annotations, which are read on the name as written (Known bugs). +The typed-array types are abbreviations of one type, `TypedArray.t<_>`, and +are told apart only by their annotations, which are read on the name as +written (Known bugs). - Implementation: `variant_layout.ml:runtime_annotations`, `declared_block_type`, `check_manifest_annotation`; `ast_untagged_variants.ml:process_runtime_annotation`, `unsupported_class`, - `check_runtime_annotation`, - `type_to_instanceof_backed_obj`; + `check_runtime_annotation`; `variant_runtime.ml:Instance`; `js_exp_make.ml:class_constructor`. - Tests: `spec_test` rules 4 and 5; `tests/tests/src/untagged_runtime_annotations_test.res`, `untagged_runtime_annotations_shadow_test.res`, `untagged_file_blob_test.res`; the fixtures above, `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res`. -- Shaped by #6296, #6383, #6396, #7788, #8009, #8950, #8948. Related: #7147. +- Shaped by #6296, #6383, #6396, #7788, #8009, #8950, #8948, #8949. + Related: #7147. ### 6. Well-formedness @@ -537,8 +524,8 @@ had to answer, and the answers of rule 5: 7. **Implied `@notUndefined`.** A declared class or kind implies it, and rule 5 rejects annotations on abbreviations of types that can hold `undefined`. -8. **Abbreviations.** Annotations are read along the chain of abbreviations; - the list of rule 5 is still read on the name as written. +8. **Abbreviations.** Annotations are read along the chain of abbreviations, + and a type is never recognized by its name. 9. **Globals.** A shadowed class is read through `globalThis`. A missing class throws when the check runs (rule 10). From c0141e690e4949240f4fff2eca73434ee220b393 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 17:25:30 +0000 Subject: [PATCH 10/11] Store the resolved runtime shape on type declarations Why: Every reader re-derived what `@instanceOf`, `@typeOf` and `@notUndefined` state by walking a type's expansion chain in the reader's environment. Interface inclusion therefore evaluated an interface's `@instanceOf("Dog") type dog = private animal` against the implementation's annotated `animal`, while clients saw an unannotated `animal`: a superclass hidden that way was lost in clients. A declaration now carries `type_shape`: `Unstated`, `Never_undefined`, or `Recognized_as` a class with its superclasses or a `typeof` kind, resolved when the type is declared, in the environment it is declared in. The classifier, `option` unboxing and inclusion read it, walking only to the first declaration that states a shape; no attribute is parsed after declaration, and the deduplication of annotations repeated through module aliases is gone. Diagnostic added (a superclass hidden by its interface): Example: // Classes.res @instanceOf("Animal") type animal @instanceOf("Dog") type dog = private animal // Classes.resi type animal @instanceOf("Dog") type dog = private animal After (diagnostic): Their runtime representations differ: the first declaration states instances of Dog, a subclass of Animal, the second declaration states instances of Dog. Review: Why a field rather than a normalized attribute? The facts are typed data, not text, and a field cannot be printed or forwarded by tools that handle attributes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel # Conflicts: # compiler/ext/config.ml --- compiler/ml/UntaggedVariants.md | 30 ++-- compiler/ml/ast_untagged_variants.ml | 3 + compiler/ml/ctype.ml | 2 + compiler/ml/datarepr.ml | 1 + compiler/ml/includecore.ml | 17 ++- compiler/ml/predef.ml | 2 + compiler/ml/printtyp.ml | 1 + compiler/ml/subst.ml | 1 + compiler/ml/typecore.ml | 1 + compiler/ml/typedecl.ml | 17 ++- compiler/ml/typedecl.mli | 1 - compiler/ml/typemod.ml | 1 + compiler/ml/typeopt.ml | 7 +- compiler/ml/types.ml | 3 + compiler/ml/types.mli | 3 + compiler/ml/variant_layout.ml | 143 ++++++++++-------- compiler/ml/variant_runtime.ml | 12 ++ compiler/ml/variant_runtime.mli | 12 ++ ...ss_runtime_annotation_hidden_base.expected | 22 +++ .../Classes.res | 2 + .../Classes.resi | 2 + .../ounit_object_mutability_tests.ml | 1 + 22 files changed, 194 insertions(+), 90 deletions(-) create mode 100644 tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_base.expected create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.resi diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 46143d4a542..dc40b99602f 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -103,7 +103,7 @@ is the number kind, and `@typeOf("symbol")` is a kind of its own, with no builtin payload type. `@notUndefined` on an abstract type leaves it unknown. - Implementation: `variant_layout.ml:get_block_type_from_typ`, - `get_block_type`, `declared_block_type`. + `get_block_type`, `recognized_as`. - Tests: `spec_test` rules 4 (`kinds`, `tuples`, `aliasDict`, `instances`, `unknown`); fixtures `UntaggedConstructorMoreThanOneArg.res`, `UntaggedConstructorAttributeMoreThanOneArg.res`, `UntaggedNonUnary1.res`, @@ -123,18 +123,24 @@ global class at the path `C`, such as `"Date"` or `"Intl.Locale"`. `"object"`. A type has at most one of the two, and only abstract types and type abbreviations, public or private, can have them. -The classifier reads the annotations of the payload type as written and of -each type it abbreviates; the outermost one decides. The classes annotated -further down a chain of abbreviations are the superclasses of the outermost -one (rule 8): +When a type is declared, its annotation is resolved into its *shape*, which +is stored on the declaration: the class with its superclasses, or the +`typeof` kind (`@notUndefined` gives the shape "never undefined", rule 13). +The classes annotated further down a chain of abbreviations are the +superclasses (rule 8): ```rescript @instanceOf("Animal") type animal @instanceOf("Dog") type dog = private animal ``` -A module alias repeats the declarations of the aliased module, so an -annotation equal to the one just read is not counted again. +The classifier and every other reader take the shape of the payload type as +written, or of the first type it abbreviates that states one, so the +outermost annotation decides. A shape is resolved in the environment the +type is declared in, so an interface's shape is what its users see, even +where the implementation knows more (rule 14). An annotation that repeats +the abbreviated type's, as the declarations of a module alias do, adds no +superclass. An annotation on an abbreviation must agree with the abbreviated type. It can refine an abstract type that is not builtin and has no annotation, or repeat @@ -187,8 +193,9 @@ The typed-array types are abbreviations of one type, `TypedArray.t<_>`, and are told apart only by their annotations, which are read on the name as written (Known bugs). -- Implementation: `variant_layout.ml:runtime_annotations`, - `declared_block_type`, `check_manifest_annotation`; +- Implementation: `types.mli:type_shape`, + `variant_runtime.mli:value_shape`; `variant_layout.ml:declaration_shape`, + `resolve_shapes`, `value_shape`, `check_manifest_annotation`; `ast_untagged_variants.ml:process_runtime_annotation`, `unsupported_class`, `check_runtime_annotation`; `variant_runtime.ml:Instance`; `js_exp_make.ml:class_constructor`. @@ -419,7 +426,9 @@ declaration it is an error, "@notUndefined can only be used on abstract types". Interface inclusion does not compare it. `@instanceOf` and `@typeOf` (rule 5) imply it. -- Implementation: `typedecl.ml:transl_declaration`, `is_not_undefined_attr`; +- Implementation: `typedecl.ml:transl_declaration`; + `ast_untagged_variants.ml:has_not_undefined`; + `variant_layout.ml:declaration_shape`; `typeopt.ml:type_cannot_contain_undefined`. - Tests: `spec_test` rule 13; fixture `invalid_attribute_not_undefined.res`; `tests/build_tests/not_undefined_attribute`. @@ -452,6 +461,7 @@ arguments, `with type` constraints and module type ascriptions. `Iface_unboxed_variant_mismatch`, `Iface_tag_name_mismatch`, `Iface_variant_representation_mismatch`, `Cross_runtime_annotation_hidden_superclass`, + `Cross_runtime_annotation_hidden_base`, `Cross_runtime_annotation_claimed_by_interface`, `Cross_runtime_annotation_functor_argument`. - Shaped by #6498, #6513, #6669, #7875. diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index 0a7827d8bc4..02f1839722b 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -316,6 +316,9 @@ let describe_declared_block_type = function "values whose typeof is \"" ^ typeof_name block_type ^ "\"" | None -> "no runtime class or typeof" +let has_not_undefined (attrs : Parsetree.attributes) = + Ext_list.exists attrs (fun ({txt}, _) -> txt = "notUndefined") + (* The [@instanceOf] or [@typeOf] annotation in [attrs], validated. Only [Typedecl] passes [~loc]; later readers see attributes it accepted. *) let process_runtime_annotation ?(loc = Location.none) diff --git a/compiler/ml/ctype.ml b/compiler/ml/ctype.ml index 3d79e206612..51d926eaae2 100644 --- a/compiler/ml/ctype.ml +++ b/compiler/ml/ctype.ml @@ -928,6 +928,7 @@ let new_declaration newtype manifest = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } @@ -4312,6 +4313,7 @@ let nondep_type_decl env mid id is_covariant decl = type_attributes = decl.type_attributes; type_immediate = decl.type_immediate; type_representation = decl.type_representation; + type_shape = decl.type_shape; type_inlined_types = decl.type_inlined_types; }) diff --git a/compiler/ml/datarepr.ml b/compiler/ml/datarepr.ml index e0e20907875..0bca237e066 100644 --- a/compiler/ml/datarepr.ml +++ b/compiler/ml/datarepr.ml @@ -91,6 +91,7 @@ let constructor_args priv cd_args cd_res path rep = type_attributes = []; type_immediate = false; type_representation; + type_shape = Unstated; type_inlined_types = []; } in diff --git a/compiler/ml/includecore.ml b/compiler/ml/includecore.ml index e06683b891e..82208e59bdb 100644 --- a/compiler/ml/includecore.ml +++ b/compiler/ml/includecore.ml @@ -334,10 +334,19 @@ and compare_records env params1_ params2_ n_ (* Values are recognized by the class or [typeof] the second declaration states, so the first must state the same. Stating nothing hides it. *) let compare_runtime_annotations env decl1 decl2 = - match Variant_layout.declaration_block_type ~env decl2 with - | None -> [] - | Some _ as declared2 -> - let declared1 = Variant_layout.declaration_block_type ~env decl1 in + (* The second declaration's shape was resolved in its own environment, so + it is what users of the second declaration see *) + match decl2.type_shape with + | Unstated | Never_undefined -> [] + | Recognized_as block_type2 -> + let declared2 = Some block_type2 in + let declared1 = + match decl1.type_shape with + | Recognized_as block_type1 -> Some block_type1 + | Never_undefined -> None + | Unstated -> + Option.bind decl1.type_manifest (Variant_layout.recognized_as ~env) + in if declared1 = declared2 then [] else [ diff --git a/compiler/ml/predef.ml b/compiler/ml/predef.ml index de6f65ee624..8ed35b7056d 100644 --- a/compiler/ml/predef.ml +++ b/compiler/ml/predef.ml @@ -215,6 +215,7 @@ let decl_abstr = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } @@ -352,6 +353,7 @@ let common_initial_env add_type add_extension empty_env = }; ]; type_representation = Types.Unboxed; + type_shape = Unstated; } and decl_promise = let tvar = newgenvar () in diff --git a/compiler/ml/printtyp.ml b/compiler/ml/printtyp.ml index 34788d96e7c..376741f0d69 100644 --- a/compiler/ml/printtyp.ml +++ b/compiler/ml/printtyp.ml @@ -1079,6 +1079,7 @@ let dummy = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } diff --git a/compiler/ml/subst.ml b/compiler/ml/subst.ml index 0ba01289e88..ea599a6df96 100644 --- a/compiler/ml/subst.ml +++ b/compiler/ml/subst.ml @@ -283,6 +283,7 @@ let type_declaration s decl = type_attributes = attrs s decl.type_attributes; type_immediate = decl.type_immediate; type_representation = decl.type_representation; + type_shape = decl.type_shape; type_inlined_types = decl.type_inlined_types; }) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 02399c5683f..84d7d9aa95b 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -3751,6 +3751,7 @@ and type_newtype ~loc ~env ~name (type_body : Env.t -> Typedtree.expression) = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } in diff --git a/compiler/ml/typedecl.ml b/compiler/ml/typedecl.ml index 3c79714d375..8ae1dc5d37f 100644 --- a/compiler/ml/typedecl.ml +++ b/compiler/ml/typedecl.ml @@ -97,6 +97,7 @@ let enter_type rec_flag env sdecl id = type_attributes = sdecl.ptype_attributes; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } in @@ -362,11 +363,6 @@ 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 @@ -376,7 +372,7 @@ let is_not_undefined_attr (attr : attribute) = let transl_declaration ~type_record_as_object env sdecl id = (* Check for @notUndefined attribute *) let has_not_undefined = - List.exists is_not_undefined_attr sdecl.ptype_attributes + Ast_untagged_variants.has_not_undefined sdecl.ptype_attributes in (if has_not_undefined then match (sdecl.ptype_kind, sdecl.ptype_manifest) with @@ -715,6 +711,7 @@ let transl_declaration ~type_record_as_object env sdecl id = type_attributes = sdecl.ptype_attributes; type_immediate = false; type_representation = representation; + type_shape = Unstated; type_inlined_types = []; } in @@ -1543,6 +1540,9 @@ let transl_type_decl env rec_flag sdecl_list = current_slot := None; (* Check for duplicates *) check_duplicates sdecl_list; + (* What each declaration states about its values at run time, resolved in + the environment it is declared in *) + let decls = Variant_layout.resolve_shapes ~env decls in (* Build the final env. *) let newenv = List.fold_right @@ -2015,6 +2015,7 @@ let transl_with_constraint env id row_path orig_decl sdecl = type_attributes = sdecl.ptype_attributes; type_immediate = false; type_representation; + type_shape = Unstated; type_inlined_types = []; } in @@ -2024,6 +2025,9 @@ let transl_with_constraint env id row_path orig_decl sdecl = (match Ctype.closed_type_decl decl with | None -> () | Some ty -> raise (Error (sdecl.ptype_loc, Unbound_type_var (ty, decl)))); + let decl = + {decl with type_shape = Variant_layout.declaration_shape ~env decl} + in Variant_layout.check_manifest_annotation ~loc:sdecl.ptype_loc ~env decl; let decl = name_recursion sdecl id decl in let type_variance = @@ -2067,6 +2071,7 @@ let abstract_type_decl arity = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } in diff --git a/compiler/ml/typedecl.mli b/compiler/ml/typedecl.mli index 2bb43c4fdbc..f12a53a9670 100644 --- a/compiler/ml/typedecl.mli +++ b/compiler/ml/typedecl.mli @@ -61,7 +61,6 @@ 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 error diff --git a/compiler/ml/typemod.ml b/compiler/ml/typemod.ml index a7cdeba926b..352e283c349 100644 --- a/compiler/ml/typemod.ml +++ b/compiler/ml/typemod.ml @@ -330,6 +330,7 @@ let merge_constraint initial_env loc sg constr = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } and id_row = Ident.create (s ^ "#row") in diff --git a/compiler/ml/typeopt.ml b/compiler/ml/typeopt.ml index d20ab709324..8cae9b2ec03 100644 --- a/compiler/ml/typeopt.ml +++ b/compiler/ml/typeopt.ml @@ -41,10 +41,10 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) = (* A value declared to be a class instance or to have a [typeof] other than "undefined" is not undefined. The payload of an [@unboxed] wrapper is declared only after scraping. *) - Option.is_some (Variant_layout.declared_block_type ~env typ) + Variant_layout.value_shape ~env typ <> Unstated || let scraped = scrape_ty env typ in - Option.is_some (Variant_layout.declared_block_type ~env scraped) + Variant_layout.value_shape ~env scraped <> Unstated || match scraped.desc with | Tconstr (p, _, _) -> ( @@ -61,8 +61,7 @@ let rec type_cannot_contain_undefined (typ : Types.type_expr) (env : Env.t) = (* A transitive dependency's interface may not be on the search path. Keep option wrapping when its runtime representation is unknown. *) false - | {type_kind = Type_abstract; type_attributes} -> - List.exists Typedecl.is_not_undefined_attr type_attributes + | {type_kind = Type_abstract; type_shape} -> type_shape <> Unstated | {type_kind = Type_open} -> false | {type_kind = Type_record _} -> true | { diff --git a/compiler/ml/types.ml b/compiler/ml/types.ml index 9808899d8fa..55208050a71 100644 --- a/compiler/ml/types.ml +++ b/compiler/ml/types.ml @@ -140,6 +140,9 @@ type type_declaration = { type_attributes: Parsetree.attributes; type_immediate: bool; type_representation: type_representation; + type_shape: Variant_runtime.value_shape; + (** Resolved from the declaration's annotations in its own + environment, when it is declared *) type_inlined_types: type_inlined_type list; } diff --git a/compiler/ml/types.mli b/compiler/ml/types.mli index a40961b5d61..e9d059a0cb7 100644 --- a/compiler/ml/types.mli +++ b/compiler/ml/types.mli @@ -236,6 +236,9 @@ type type_declaration = { type_attributes: Parsetree.attributes; type_immediate: bool; (* true iff type should not be a pointer *) type_representation: type_representation; + type_shape: Variant_runtime.value_shape; + (** Resolved from the declaration's annotations in its own + environment, when it is declared *) type_inlined_types: type_inlined_type list; (** Representation of inlined types, needed for printing *) } diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index 75652afd396..63ea4e9d862 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -8,71 +8,81 @@ open Variant_runtime open Ast_untagged_variants -(* The [@instanceOf] and [@typeOf] annotations of [t] and of the types it - abbreviates, outermost first. The type as written can carry one, and so can - each level of a chain of private abbreviations that models a class - hierarchy, such as [@instanceOf("File") type t = private Blob.t]. *) -let runtime_annotations ~env (t : Types.type_expr) = - let rec collect ~previous (t : Types.type_expr) = +(* The shape of [t]: the one stated by the first declaration on its expansion + chain that states one. Each declaration's shape already includes what the + types it abbreviates state, such as a class's superclasses. [fuel] bounds + the walk, since a recursive group's shapes are resolved before its + abbreviations are checked for cycles. *) +let value_shape ~env (t : Types.type_expr) : value_shape = + let rec walk fuel (t : Types.type_expr) = match (Btype.repr t).desc with - | Tconstr (path, _, _) -> - let here = - match Env.find_type path env with - | {type_attributes} -> process_runtime_annotation type_attributes - | exception Not_found -> None - in - (* A module alias makes [M.t] an abbreviation of the original [t] with - the same attributes, so an annotation repeating the one just seen is - the same declaration again *) - let here, previous = - match here with - | Some _ when here = previous -> (None, previous) - | Some _ -> (here, here) - | None -> (None, previous) - in - let rest = + | Tconstr (path, _, _) when fuel > 0 -> ( + match Env.find_type path env with + | {type_shape = (Never_undefined | Recognized_as _) as shape} -> shape + | {type_shape = Unstated} | (exception Not_found) -> ( match Ctype.try_expand_once_opt env t with - | expanded -> collect ~previous expanded - | exception Ctype.Cannot_expand -> [] - in - Option.to_list here @ rest - | _ -> [] + | expanded -> walk (fuel - 1) expanded + | exception Ctype.Cannot_expand -> Unstated)) + | _ -> Unstated in - collect ~previous:None t + walk 100 t + +(* The payload kind [t] is declared to have *) +let recognized_as ~env t = + match value_shape ~env t with + | Recognized_as block_type -> Some block_type + | Unstated | Never_undefined -> None -(* The block type the outermost of [annotations] states. A class's ancestors - are the classes annotated further down the chain. *) -let block_type_of_annotations = function - | Instance_of name :: rest -> +(* The shape a declaration states, resolved against what the type it + abbreviates states in [env]: a class's superclasses are the classes + annotated further down a chain of abbreviations. *) +let declaration_shape ~env (decl : Types.type_declaration) : value_shape = + match process_runtime_annotation decl.type_attributes with + | Some (Type_of block_type) -> Recognized_as block_type + | Some (Instance_of name) -> let ancestors = - List.filter_map - (function - | Instance_of ancestor when ancestor <> name -> Some ancestor - | Instance_of _ | Type_of _ -> None) - rest + match Option.map (recognized_as ~env) decl.type_manifest with + | Some (Some (InstanceType (Class {name = inner; ancestors}))) -> + (* An annotation repeating the abbreviated type's, as a module alias + does, states no new class *) + if inner = name then ancestors else inner :: ancestors + | Some _ | None -> [] in - Some (InstanceType (Class {name; ancestors})) - | Type_of block_type :: _ -> Some block_type - | [] -> None + Recognized_as (InstanceType (Class {name; ancestors})) + | None -> + if has_not_undefined decl.type_attributes then Never_undefined else Unstated -let declared_block_type ~env t = - block_type_of_annotations (runtime_annotations ~env t) - -(* The block type a declaration states, through its own annotation or the - ones of the type it abbreviates. *) -let declaration_block_type ~env (decl : Types.type_declaration) = - let own = process_runtime_annotation decl.type_attributes in - let inner = - match decl.type_manifest with - | Some manifest -> runtime_annotations ~env manifest - | None -> [] +(* Resolves the shapes of a group of declarations, which can abbreviate each + other. Each round resolves one more level of abbreviation within the + group, and a chain inside the group has at most one level per + declaration. *) +let resolve_shapes ~env (decls : (Ident.t * Types.type_declaration) list) = + let rec round env decls n = + let decls = + List.map + (fun (id, decl) -> + (id, {decl with Types.type_shape = declaration_shape ~env decl})) + decls + in + if n <= 1 then decls + else + let env = + List.fold_left + (fun env (id, decl) -> Env.add_type ~check:false id decl env) + env decls + in + round env decls (n - 1) in - let inner = - match inner with - | annotation :: rest when Some annotation = own -> rest - | _ -> inner + let env = + List.fold_left + (fun env (id, decl) -> Env.add_type ~check:false id decl env) + env decls in - block_type_of_annotations (Option.to_list own @ inner) + round env decls (List.length decls) + +let annotation_of_block_type = function + | InstanceType (Class {name}) -> Instance_of name + | block_type -> Type_of block_type (* An annotation on an abbreviation is a claim about the abbreviated type, so it must agree with what the compiler knows about that type. It can refine @@ -89,8 +99,8 @@ let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = | None, _ | Some _, None -> () | Some outer, Some manifest -> ( let error err = raise (Error (loc, err)) in - match (outer, runtime_annotations ~env manifest) with - | _, [] -> ( + match (outer, recognized_as ~env manifest) with + | _, None -> ( let is_abstract path = match Env.find_type path env with | {type_kind = Type_abstract; type_manifest = None} -> true @@ -104,9 +114,12 @@ let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = () | _ -> error (RuntimeAnnotationOnUnrelatedType (runtime_attribute_name outer))) - | Type_of block_type, Type_of inner :: _ when block_type = inner -> () - | Instance_of name, Instance_of inner :: _ when name = inner -> () - | Instance_of name, (Instance_of ancestor :: _ as chain) -> + | Type_of block_type, Some inner when block_type = inner -> () + | Instance_of name, Some (InstanceType (Class {name = inner})) + when name = inner -> + () + | Instance_of name, Some (InstanceType (Class {name = ancestor; ancestors})) + -> (* A public abbreviation is the same type as the superclass, unless it is a strict instance of it, as in the phantom type hierarchy [type element<'a> = node<_element<'a>>] *) @@ -117,15 +130,15 @@ let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = in if decl.type_private = Public && same_type then error (SubclassOfPublicAbbreviation (name, ancestor)) - else if List.mem outer chain then error (ClassTwiceInChain name) - | (Instance_of _ | Type_of _), inner :: _ -> + else if List.mem name ancestors then error (ClassTwiceInChain name) + | (Instance_of _ | Type_of _), Some inner -> error (RuntimeAnnotationContradictsManifest ( runtime_annotation_to_string outer, - runtime_annotation_to_string inner ))) + runtime_annotation_to_string (annotation_of_block_type inner) ))) let get_block_type_from_typ ~env (t : Types.type_expr) : block_type option = - match declared_block_type ~env t with + match recognized_as ~env t with | Some _ as declared -> declared | None -> ( (* Builtin types have no declaration to annotate *) diff --git a/compiler/ml/variant_runtime.ml b/compiler/ml/variant_runtime.ml index 47696495d58..9670b910695 100644 --- a/compiler/ml/variant_runtime.ml +++ b/compiler/ml/variant_runtime.ml @@ -30,6 +30,18 @@ type block_type = | ObjectType | UnknownType +(** What a type declaration states about its values at run time, through + [@notUndefined], [@instanceOf] or [@typeOf]. Each level implies the one + before: a value recognized as a class instance or [typeof] kind is never + [undefined]. *) +type value_shape = + | Unstated + (** Nothing: an abbreviation has the shape of the type it abbreviates *) + | Never_undefined (** [@notUndefined] *) + | Recognized_as of block_type + (** [@instanceOf] or [@typeOf]: a payload of this kind, with the class's + declared superclasses *) + (* The literal value a constructor is represented by: what [@as] states, or the constructor's own name when it states nothing. Unlike [tag_type], this can never describe an inferred untagged payload shape. *) diff --git a/compiler/ml/variant_runtime.mli b/compiler/ml/variant_runtime.mli index 491a2fd3d35..3caad34ed63 100644 --- a/compiler/ml/variant_runtime.mli +++ b/compiler/ml/variant_runtime.mli @@ -25,6 +25,18 @@ type block_type = | ObjectType | UnknownType +(** What a type declaration states about its values at run time, through + [@notUndefined], [@instanceOf] or [@typeOf]. Each level implies the one + before: a value recognized as a class instance or [typeof] kind is never + [undefined]. *) +type value_shape = + | Unstated + (** Nothing: an abbreviation has the shape of the type it abbreviates *) + | Never_undefined (** [@notUndefined] *) + | Recognized_as of block_type + (** [@instanceOf] or [@typeOf]: a payload of this kind, with the class's + declared superclasses *) + (** The literal value a constructor is represented by: what [@as] states, or the constructor's own name when it states nothing. Unlike [tag_type], this can never describe an inferred untagged payload shape. *) diff --git a/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_base.expected b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_base.expected new file mode 100644 index 00000000000..670eba8a7d9 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Cross_runtime_annotation_hidden_base.expected @@ -0,0 +1,22 @@ +===== Classes.res ===== + + We've found a bug for you! + /.../fixtures/Cross_runtime_annotation_hidden_base/Classes.res:2:20-44 + + 1 │ @instanceOf("Animal") type animal + 2 │ @instanceOf("Dog") type dog = private animal + 3 │ + + The implementation /.../fixtures/Cross_runtime_annotation_hidden_base/Classes.res + does not match the interface /.../fixtures/Cross_runtime_annotation_hidden_base/classes.cmi: + Type declarations do not match: + type dog = private animal + is not included in + type dog = private animal + /.../fixtures/Cross_runtime_annotation_hidden_base/Classes.resi:2:20-44: + Expected declaration + /.../fixtures/Cross_runtime_annotation_hidden_base/Classes.res:2:20-44: + Actual declaration + Their runtime representations differ: + the first declaration states instances of Dog, a subclass of Animal, + the second declaration states instances of Dog. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.res b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.res new file mode 100644 index 00000000000..793a12dbd3b --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.res @@ -0,0 +1,2 @@ +@instanceOf("Animal") type animal +@instanceOf("Dog") type dog = private animal diff --git a/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.resi b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.resi new file mode 100644 index 00000000000..1da495314dc --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Cross_runtime_annotation_hidden_base/Classes.resi @@ -0,0 +1,2 @@ +type animal +@instanceOf("Dog") type dog = private animal diff --git a/tests/ounit_tests/ounit_object_mutability_tests.ml b/tests/ounit_tests/ounit_object_mutability_tests.ml index 4eb88631081..55d72c61419 100644 --- a/tests/ounit_tests/ounit_object_mutability_tests.ml +++ b/tests/ounit_tests/ounit_object_mutability_tests.ml @@ -168,6 +168,7 @@ let abstract_type_decl type_manifest : Types.type_declaration = type_attributes = []; type_immediate = false; type_representation = Boxed; + type_shape = Unstated; type_inlined_types = []; } From 3150e2f1356e94c82d18f73beef581f125433d1a Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 11:31:10 +0000 Subject: [PATCH 11/11] Close the gaps the re-review of runtime annotations found Why: An annotation could refine a functor parameter's or a locally abstract type, which stands for any type: `@instanceOf("Date") type d = X.t` in a functor applied to `option` made `Some(None)` collapse to `None`. An abbreviation through an alias passed as a phantom subclass, an interface hiding a payload's class was rejected although master accepts it, and two `@instanceOf` attributes were reported as a conflict between the two kinds. Annotations on the type of a functor parameter, of a recursive module being defined, or on a locally abstract type are rejected. The phantom-hierarchy exception reads the abbreviation's arguments after expansion. A class can be declared over a `@typeOf("object")` type. An interface case with an unknown payload agrees with any payload of the implementation, since it is the only payload case and is never checked. Diagnostic added (a type that stands for another one): Example: module Make = (X: {type t}) => { @instanceOf("Date") type date = X.t } After (diagnostic): @instanceOf cannot describe the type of a functor parameter, of a recursive module being defined, or a locally abstract type: such a type stands for another one, whose values can be anything. Diagnostic added (public subclass through an alias): Example: @instanceOf("Animal") type animal type phantom<'a> = animal @instanceOf("Dog") type dog = phantom After (diagnostic): A subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion. Diagnostic removed (an interface hiding a payload's class): Example: // A.resi type t @unboxed type v = | @as("n") N | P(t) // A.res type t = Date.t @unboxed type v = | @as("n") N | P(t) Before (diagnostic): The internal representations for case P are not equal Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel --- analysis/src/completion_decorators.ml | 2 +- compiler/ml/UntaggedVariants.md | 23 +++++--- compiler/ml/ast_untagged_variants.ml | 18 +++++- compiler/ml/includecore.ml | 4 +- compiler/ml/variant_layout.ml | 30 +++++++--- .../src/expected/CompletionAttributes.res.txt | 2 +- .../instance_of_duplicate.res.expected | 8 +++ ...instance_of_functor_parameter.res.expected | 11 ++++ ...public_subclass_through_alias.res.expected | 10 ++++ ...type_of_locally_abstract_type.res.expected | 11 ++++ .../fixtures/instance_of_duplicate.res | 1 + .../instance_of_functor_parameter.res | 7 +++ ...tance_of_public_subclass_through_alias.res | 3 + .../type_of_locally_abstract_type.res | 6 ++ .../src/untagged_hidden_payload_test.mjs | 25 ++++++++ .../src/untagged_hidden_payload_test.res | 17 ++++++ .../src/untagged_hidden_payload_test.resi | 4 ++ .../src/untagged_runtime_annotations_test.mjs | 57 ++++++++++++------- .../src/untagged_runtime_annotations_test.res | 19 +++++++ .../tests/src/untagged_variants_spec_test.res | 2 +- 20 files changed, 218 insertions(+), 42 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/instance_of_duplicate.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_functor_parameter.res.expected create mode 100644 tests/build_tests/super_errors/expected/instance_of_public_subclass_through_alias.res.expected create mode 100644 tests/build_tests/super_errors/expected/type_of_locally_abstract_type.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_duplicate.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_functor_parameter.res create mode 100644 tests/build_tests/super_errors/fixtures/instance_of_public_subclass_through_alias.res create mode 100644 tests/build_tests/super_errors/fixtures/type_of_locally_abstract_type.res create mode 100644 tests/tests/src/untagged_hidden_payload_test.mjs create mode 100644 tests/tests/src/untagged_hidden_payload_test.res create mode 100644 tests/tests/src/untagged_hidden_payload_test.resi diff --git a/analysis/src/completion_decorators.ml b/analysis/src/completion_decorators.ml index b5e5f1ccc3a..9ff723c55b8 100644 --- a/analysis/src/completion_decorators.ml +++ b/analysis/src/completion_decorators.ml @@ -303,7 +303,7 @@ You will need this decorator whenever you want to use a JSX component in ReScrip [ {|The `@instanceOf` decorator states that the values of an abstract type are instances of a global JavaScript class. Such a type can be the payload of an untagged variant case, which is then recognized with `instanceof`, and is never `undefined`, like with `@notUndefined`. -A private abbreviation of another annotated type declares a subclass, so that the subclass case is checked first. +A private abbreviation of another annotated type declares a subclass. A case of the superclass then excludes the instances of a subclass that is a case too, whatever the order of the cases. Example usage: ```rescript diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index dc40b99602f..c8ad0586c98 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -144,20 +144,27 @@ superclass. An annotation on an abbreviation must agree with the abbreviated type. It can refine an abstract type that is not builtin and has no annotation, or repeat -the annotation of the abbreviated type. Only a private abbreviation can -declare a subclass of the class annotated there, or a public one that is a -strict instance of the superclass type, as in the phantom type hierarchy +the annotation of the abbreviated type. The type of a functor parameter, of a +recursive module being defined, and a locally abstract type `(type a)` are +abstract only where they are declared: they stand for another type, so they +cannot be refined. Only a private abbreviation can declare a subclass of the +class annotated there, or of a `@typeOf("object")` type, or a public one that +is a strict instance of the superclass type after expansion, as in the +phantom type hierarchy `@instanceOf("Element") type elementLike<'a> = nodeLike<_element<'a>>`. A -public `type dog = animal` would make every animal a dog without a coercion. +public `type dog = animal` would make every animal a dog without a coercion, +and so would `type dog = phantom` with `type phantom<'a> = animal`. | Declaration | Error | Fixture | | --- | --- | --- | | `@typeOf("string") type t = option`, `type t<'a> = 'a`, an abbreviation of `unit`, a builtin type, a record or a variant | @typeOf on a type abbreviation must describe an abstract type, ... | `runtime_annotation_on_option.res`, `runtime_annotation_on_type_variable.res` | | `@typeOf("number") type t = sym` with `@typeOf("symbol") type sym` | This annotation, @typeOf("number"), contradicts the abbreviated type's annotation, @typeOf("symbol"). | `runtime_annotation_contradicts_manifest.res` | -| `@instanceOf("Dog") type dog = animal` | A subclass must be a private abbreviation ... | `instance_of_public_subclass.res` | +| `@instanceOf("Date") type date = X.t` in a functor of `X`, or over `(type a)` | @instanceOf cannot describe the type of a functor parameter, ... | `instance_of_functor_parameter.res`, `type_of_locally_abstract_type.res` | +| `@instanceOf("Dog") type dog = animal`, or `= phantom` with `type phantom<'a> = animal` | A subclass must be a private abbreviation ... | `instance_of_public_subclass.res`, `instance_of_public_subclass_through_alias.res` | | `@instanceOf("Animal") type t = private dog` | Animal appears twice in this chain of abbreviations ... | `instance_of_class_twice_in_chain.res` | | `@instanceOf` on a record or variant | @instanceOf can only be used on abstract types and type abbreviations ... | `runtime_annotation_on_record.res` | | both annotations | A type can have either @instanceOf or @typeOf, not both. | `runtime_annotation_conflict.res` | +| one annotation twice | Duplicate @instanceOf annotation; a type can have only one. | `instance_of_duplicate.res` | The class path is a dot-separated list of identifiers whose first one is not a JavaScript keyword. Paths that name no class to recognize values by are @@ -443,8 +450,10 @@ error. An interface and its implementation must agree on `@unboxed`, on `@tag`, on the literal of every nullary case, and on the payload kind of every untagged -case. They are not compared on the `@as` of cases with a payload (see Known -bugs). +case whose kind the interface knows. An unknown payload in the interface is +its only payload case and is not checked, so it agrees with any payload of +the implementation (`untagged_hidden_payload_test.res`). They are not +compared on the `@as` of cases with a payload (see Known bugs). A type for which an interface states a class or `typeof` (rule 5), through its own annotation or the type it abbreviates, must state the same one in the diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index 02f1839722b..cc37aade148 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -34,6 +34,8 @@ type error = | UnsupportedInstanceOf of string * unsupported_class | InvalidTypeOf of string | ConflictingRuntimeAnnotations + | DuplicateRuntimeAnnotation of string + | RuntimeAnnotationOnTypeParameter of string | RuntimeAnnotationOnRepresentedType of string | RuntimeAnnotationOnUnrelatedType of string | RuntimeAnnotationContradictsManifest of string * string @@ -124,6 +126,14 @@ let report_error ppf = "@typeOf expects one of \"string\", \"number\", \"bigint\", \"boolean\", \ \"symbol\", \"function\" or \"object\"%s." (if name = "" then "" else ", not \"" ^ name ^ "\"") + | DuplicateRuntimeAnnotation attribute -> + fprintf ppf "Duplicate @%s annotation; a type can have only one." attribute + | RuntimeAnnotationOnTypeParameter attribute -> + fprintf ppf + "@%s cannot describe the type of a functor parameter, of a recursive \ + module being defined, or a locally abstract type: such a type stands \ + for another one, whose values can be anything." + attribute | ConflictingRuntimeAnnotations -> fprintf ppf "A type can have either @instanceOf or @typeOf, not both." | RuntimeAnnotationOnRepresentedType attribute -> @@ -345,7 +355,13 @@ let process_runtime_annotation ?(loc = Location.none) match Ext_list.filter_map attrs annotation_of with | [] -> None | [annotation] -> Some annotation - | _ :: _ :: _ -> raise (Error (loc, ConflictingRuntimeAnnotations)) + | first :: second :: _ -> + raise + (Error + ( loc, + if runtime_attribute_name first = runtime_attribute_name second then + DuplicateRuntimeAnnotation (runtime_attribute_name first) + else ConflictingRuntimeAnnotations )) (* Checks the runtime annotations of a declaration whose kind is [is_abstract_or_abbreviation]. *) diff --git a/compiler/ml/includecore.ml b/compiler/ml/includecore.ml index 82208e59bdb..635298bd37b 100644 --- a/compiler/ml/includecore.ml +++ b/compiler/ml/includecore.ml @@ -281,9 +281,11 @@ and compare_variants ~loc env params1 params2 ~layout1 ~layout2 n ( Variant_runtime.constructor_at layout1 (n - 1), Variant_runtime.constructor_at layout2 (n - 1) ) with + (* An unknown payload in the second declaration is the only + payload case and has no check, so any first one agrees *) | ( Block (Untagged {block_type = block_type1}), Block (Untagged {block_type = block_type2}) ) - when block_type1 <> block_type2 -> + when block_type1 <> block_type2 && block_type2 <> UnknownType -> [Variant_representation cd1.cd_id] | _ -> r) in diff --git a/compiler/ml/variant_layout.ml b/compiler/ml/variant_layout.ml index 63ea4e9d862..0389d82c20d 100644 --- a/compiler/ml/variant_layout.ml +++ b/compiler/ml/variant_layout.ml @@ -101,16 +101,23 @@ let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = let error err = raise (Error (loc, err)) in match (outer, recognized_as ~env manifest) with | _, None -> ( - let is_abstract path = + (* A functor parameter's type and a locally abstract type are abstract + only here: they stand for whatever type they are instantiated with *) + let refinable path = match Env.find_type path env with - | {type_kind = Type_abstract; type_manifest = None} -> true + | {type_kind = Type_abstract; type_manifest = None; type_newtype_level} + -> + if type_newtype_level <> None || Env.is_functor_arg path env then + error + (RuntimeAnnotationOnTypeParameter (runtime_attribute_name outer)) + else true | _ -> false | exception Not_found -> false in match (Ctype.expand_head_opt env manifest).desc with | Tconstr (path, _, _) when Predef.type_is_builtin_path_but_option path = Predef.NA - && is_abstract path -> + && refinable path -> () | _ -> error (RuntimeAnnotationOnUnrelatedType (runtime_attribute_name outer))) @@ -118,18 +125,25 @@ let check_manifest_annotation ~loc ~env (decl : Types.type_declaration) = | Instance_of name, Some (InstanceType (Class {name = inner})) when name = inner -> () - | Instance_of name, Some (InstanceType (Class {name = ancestor; ancestors})) - -> + | Instance_of name, Some ((InstanceType (Class _) | ObjectType) as inner) -> (* A public abbreviation is the same type as the superclass, unless it is a strict instance of it, as in the phantom type hierarchy - [type element<'a> = node<_element<'a>>] *) + [type element<'a> = node<_element<'a>>]. The arguments are read after + expansion, where the superclass type is reached. *) let same_type = - match (Btype.repr manifest).desc with + match (Ctype.expand_head_opt env manifest).desc with | Tconstr (_, args, _) -> List.for_all Btype.is_Tvar args | _ -> true in + let superclass, ancestors = + match inner with + | InstanceType (Class {name = ancestor; ancestors}) -> + (ancestor, ancestor :: ancestors) + (* Every class instance is an object *) + | _ -> ("Object", []) + in if decl.type_private = Public && same_type then - error (SubclassOfPublicAbbreviation (name, ancestor)) + error (SubclassOfPublicAbbreviation (name, superclass)) else if List.mem name ancestors then error (ClassTwiceInChain name) | (Instance_of _ | Type_of _), Some inner -> error diff --git a/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt b/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt index 2797cafdf06..4d75cee7319 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionAttributes.res.txt @@ -325,7 +325,7 @@ Resolved opens 1 Stdlib "detail": "", "documentation": { "kind": "markdown", - "value": "The `@instanceOf` decorator states that the values of an abstract type are instances of a global JavaScript class. Such a type can be the payload of an untagged variant case, which is then recognized with `instanceof`, and is never `undefined`, like with `@notUndefined`.\n\nA private abbreviation of another annotated type declares a subclass, so that the subclass case is checked first.\n\nExample usage:\n```rescript\n@instanceOf(\"Blob\")\ntype blob\n\n@instanceOf(\"File\")\ntype file = private blob\n```" + "value": "The `@instanceOf` decorator states that the values of an abstract type are instances of a global JavaScript class. Such a type can be the payload of an untagged variant case, which is then recognized with `instanceof`, and is never `undefined`, like with `@notUndefined`.\n\nA private abbreviation of another annotated type declares a subclass. A case of the superclass then excludes the instances of a subclass that is a case too, whatever the order of the cases.\n\nExample usage:\n```rescript\n@instanceOf(\"Blob\")\ntype blob\n\n@instanceOf(\"File\")\ntype file = private blob\n```" }, "insertText": "instanceOf(\"$0\")", "insertTextFormat": 2, diff --git a/tests/build_tests/super_errors/expected/instance_of_duplicate.res.expected b/tests/build_tests/super_errors/expected/instance_of_duplicate.res.expected new file mode 100644 index 00000000000..b85dd315b2a --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_duplicate.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/instance_of_duplicate.res:1:43-48 + + 1 │ @instanceOf("Date") @instanceOf("RegExp") type t + 2 │ + + Duplicate @instanceOf annotation; a type can have only one. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_functor_parameter.res.expected b/tests/build_tests/super_errors/expected/instance_of_functor_parameter.res.expected new file mode 100644 index 00000000000..9d9168988d9 --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_functor_parameter.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/instance_of_functor_parameter.res:6:23-37 + + 4 │ }, + 5 │ ) => { + 6 │ @instanceOf("Date") type date = X.t + 7 │ } + 8 │ + + @instanceOf cannot describe the type of a functor parameter, of a recursive module being defined, or a locally abstract type: such a type stands for another one, whose values can be anything. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/instance_of_public_subclass_through_alias.res.expected b/tests/build_tests/super_errors/expected/instance_of_public_subclass_through_alias.res.expected new file mode 100644 index 00000000000..52d59e6bbef --- /dev/null +++ b/tests/build_tests/super_errors/expected/instance_of_public_subclass_through_alias.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/instance_of_public_subclass_through_alias.res:3:20-42 + + 1 │ @instanceOf("Animal") type animal + 2 │ type phantom<'a> = animal + 3 │ @instanceOf("Dog") type dog = phantom + 4 │ + + A subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/type_of_locally_abstract_type.res.expected b/tests/build_tests/super_errors/expected/type_of_locally_abstract_type.res.expected new file mode 100644 index 00000000000..1a73a599dc5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/type_of_locally_abstract_type.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/type_of_locally_abstract_type.res:3:23-32 + + 1 │ let f = (type a, x: a) => { + 2 │ module M = { + 3 │ @typeOf("object") type o = a + 4 │ } + 5 │ x + + @typeOf cannot describe the type of a functor parameter, of a recursive module being defined, or a locally abstract type: such a type stands for another one, whose values can be anything. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/instance_of_duplicate.res b/tests/build_tests/super_errors/fixtures/instance_of_duplicate.res new file mode 100644 index 00000000000..f8bb7dca404 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_duplicate.res @@ -0,0 +1 @@ +@instanceOf("Date") @instanceOf("RegExp") type t diff --git a/tests/build_tests/super_errors/fixtures/instance_of_functor_parameter.res b/tests/build_tests/super_errors/fixtures/instance_of_functor_parameter.res new file mode 100644 index 00000000000..7c9ee8be349 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_functor_parameter.res @@ -0,0 +1,7 @@ +module Make = ( + X: { + type t + }, +) => { + @instanceOf("Date") type date = X.t +} diff --git a/tests/build_tests/super_errors/fixtures/instance_of_public_subclass_through_alias.res b/tests/build_tests/super_errors/fixtures/instance_of_public_subclass_through_alias.res new file mode 100644 index 00000000000..ac8fe57c97f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/instance_of_public_subclass_through_alias.res @@ -0,0 +1,3 @@ +@instanceOf("Animal") type animal +type phantom<'a> = animal +@instanceOf("Dog") type dog = phantom diff --git a/tests/build_tests/super_errors/fixtures/type_of_locally_abstract_type.res b/tests/build_tests/super_errors/fixtures/type_of_locally_abstract_type.res new file mode 100644 index 00000000000..3c0a055c6b5 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/type_of_locally_abstract_type.res @@ -0,0 +1,6 @@ +let f = (type a, x: a) => { + module M = { + @typeOf("object") type o = a + } + x +} diff --git a/tests/tests/src/untagged_hidden_payload_test.mjs b/tests/tests/src/untagged_hidden_payload_test.mjs new file mode 100644 index 00000000000..aa6c3e0c19b --- /dev/null +++ b/tests/tests/src/untagged_hidden_payload_test.mjs @@ -0,0 +1,25 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function make() { + return new Date(); +} + +function isPayload(v) { + return v !== "n"; +} + +Mocha.describe("Untagged_hidden_payload_test", () => { + Mocha.test("an interface hiding a payload's class", () => { + Test_utils.eq("File \"untagged_hidden_payload_test.res\", line 14, characters 18-25", true, isPayload(new Date())); + Test_utils.eq("File \"untagged_hidden_payload_test.res\", line 15, characters 18-25", false, false); + }); +}); + +export { + make, + isPayload, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_hidden_payload_test.res b/tests/tests/src/untagged_hidden_payload_test.res new file mode 100644 index 00000000000..b5119bdd33e --- /dev/null +++ b/tests/tests/src/untagged_hidden_payload_test.res @@ -0,0 +1,17 @@ +// The interface hides the payload type, so its case P has an unknown payload, +// which agrees with the implementation's class payload +type t = Date.t +@unboxed type v = | @as("n") N | P(t) +let make = () => P(Date.make()) +let isPayload = v => + switch v { + | N => false + | P(_) => true + } + +Mocha.describe(__MODULE__, () => { + Mocha.test("an interface hiding a payload's class", () => { + Test_utils.eq(__LOC__, true, isPayload(make())) + Test_utils.eq(__LOC__, false, isPayload(N)) + }) +}) diff --git a/tests/tests/src/untagged_hidden_payload_test.resi b/tests/tests/src/untagged_hidden_payload_test.resi new file mode 100644 index 00000000000..7ebf3a46fd1 --- /dev/null +++ b/tests/tests/src/untagged_hidden_payload_test.resi @@ -0,0 +1,4 @@ +type t +@unboxed type v = | @as("n") N | P(t) +let make: unit => v +let isPayload: v => bool diff --git a/tests/tests/src/untagged_runtime_annotations_test.mjs b/tests/tests/src/untagged_runtime_annotations_test.mjs index 19be267ff0f..bb679921349 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.mjs +++ b/tests/tests/src/untagged_runtime_annotations_test.mjs @@ -94,52 +94,64 @@ function handlerKind(handler) { } } +function objectOrNameKind(value) { + if (value instanceof UntaggedDog) { + return "dog"; + } else { + return value; + } +} + Mocha.describe("Untagged_runtime_annotations_test", () => { Mocha.test("a subclass case is not taken for its superclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 134, characters 7-14", "animal", describePet(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 135, characters 7-14", "dog", describePet(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 136, characters 7-14", "rex", "rex"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 148, characters 7-14", "animal", describePet(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 149, characters 7-14", "dog", describePet(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 150, characters 7-14", "rex", "rex"); }); Mocha.test("a superclass case alone excludes the subclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 140, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 141, characters 7-14", "other", onlyAnimal(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 154, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 155, characters 7-14", "other", onlyAnimal(new UntaggedDog())); }); Mocha.test("declaration order does not matter", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 145, characters 7-14", "dog", describeReversed(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 146, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 159, characters 7-14", "dog", describeReversed(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 160, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); }); Mocha.test("a subclass upcasts to its superclass", () => { let animal = new UntaggedDog(); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 151, characters 7-14", "dog", describePet(animal)); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 165, characters 7-14", "dog", describePet(animal)); }); Mocha.test("classes reached through a module alias", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 155, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 156, characters 7-14", "dog", describeAliased(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 169, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 170, characters 7-14", "dog", describeAliased(new UntaggedDog())); }); Mocha.test("typeOf symbol", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 160, characters 7-14", "symbol", keyKind(Symbol.for("key"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 161, characters 7-14", "text", "text"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 174, characters 7-14", "symbol", keyKind(Symbol.for("key"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 175, characters 7-14", "text", "text"); }); Mocha.test("typeOf string", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 165, characters 7-14", "id", "id"); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 166, characters 7-14", "index", "index"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 179, characters 7-14", "id", "id"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 180, characters 7-14", "index", "index"); }); Mocha.test("a class at a dotted path", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 170, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 171, characters 7-14", "de", "de"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 184, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 185, characters 7-14", "de", "de"); }); Mocha.test("an annotated type is not wrapped in an option", () => { let animal = new UntaggedAnimal(); - Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 176, characters 7-14", animal === animal); - Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 177, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 190, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 191, characters 7-14", animal === animal); }); Mocha.test("a phantom type hierarchy", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 181, characters 7-14", "animal", describePhantom(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 182, characters 7-14", "dog", describePhantom(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 195, characters 7-14", "animal", describePhantom(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 196, characters 7-14", "dog", describePhantom(new UntaggedDog())); }); Mocha.test("a function case excludes class instances", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 186, characters 7-14", "callable", handlerKind(new UntaggedCallable())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 187, characters 7-14", "function", handlerKind(x => x)); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 200, characters 7-14", "callable", handlerKind(new UntaggedCallable())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 201, characters 7-14", "function", handlerKind(x => x)); + }); + Mocha.test("a class declared over an object type", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 205, characters 7-14", "dog", objectOrNameKind(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 206, characters 7-14", "rex", "rex"); }); }); @@ -157,5 +169,6 @@ export { localeKind, describePhantom, handlerKind, + objectOrNameKind, } /* Not a pure module */ diff --git a/tests/tests/src/untagged_runtime_annotations_test.res b/tests/tests/src/untagged_runtime_annotations_test.res index 2c8f99d4d9f..d7e53b83cfa 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.res +++ b/tests/tests/src/untagged_runtime_annotations_test.res @@ -129,6 +129,20 @@ let handlerKind = handler => @unboxed type boxedAnimal = BoxedAnimal(animal) +// A class declared over a type whose values are objects +@typeOf("object") type plainObject +@instanceOf("UntaggedDog") type objectDog = private plainObject +@new external makeObjectDog: unit => objectDog = "UntaggedDog" + +@unboxed +type objectOrName = ObjectDog(objectDog) | ObjectName(string) + +let objectOrNameKind = value => + switch value { + | ObjectDog(_) => "dog" + | ObjectName(name) => name + } + describe(__MODULE__, () => { test("a subclass case is not taken for its superclass", () => { eq(__LOC__, "animal", describePet(Animal(makeAnimal()))) @@ -186,4 +200,9 @@ describe(__MODULE__, () => { eq(__LOC__, "callable", handlerKind(Callable(makeCallable()))) eq(__LOC__, "function", handlerKind(Function(x => x))) }) + + test("a class declared over an object type", () => { + eq(__LOC__, "dog", objectOrNameKind(ObjectDog(makeObjectDog()))) + eq(__LOC__, "rex", objectOrNameKind(ObjectName("rex"))) + }) }) diff --git a/tests/tests/src/untagged_variants_spec_test.res b/tests/tests/src/untagged_variants_spec_test.res index 9f7bec046c9..10589cc5075 100644 --- a/tests/tests/src/untagged_variants_spec_test.res +++ b/tests/tests/src/untagged_variants_spec_test.res @@ -2,7 +2,7 @@ open Mocha open Test_utils /*** -Pins the rules stated in compiler/ml/UNTAGGED_VARIANTS.md. Each block names +Pins the rules stated in compiler/ml/UntaggedVariants.md. Each block names the rule it pins. Values are passed through [id], whose [@inline(never)] keeps them away from constant folding, so the assertions observe the generated dispatch. [raw] injects a JavaScript value a case's type does not describe;