diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index 43967a3f1ce..1ad6b79939f 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -932,7 +932,7 @@ and expression_desc cxt ~(level : int) f x : cxt = else ( Js_op.Lit tag_name, (* TAG:xx for inline records *) - match Ast_untagged_variants.process_tag_type p.attrs with + match Ast_untagged_variants.process_constructor_tag_type p.attrs with | None -> E.str p.name | Some t -> E.tag_type t ) :: tails @@ -940,7 +940,7 @@ and expression_desc cxt ~(level : int) f x : cxt = expression_desc cxt ~level f (Object (None, objs)) | Caml_block (el, _, tag, Blk_constructor p) -> let not_is_cons = p.name <> Literals.cons in - let tag_type = Ast_untagged_variants.process_tag_type p.attrs in + let tag_type = Ast_untagged_variants.process_constructor_tag_type p.attrs in let untagged = Ast_untagged_variants.process_untagged p.attrs in let tag_name = match Ast_untagged_variants.process_tag_name p.attrs with diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 42844bc99c0..cee795a0042 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -168,9 +168,10 @@ let get_block_cases (sw_names : Ast_untagged_variants.switch_names option) = (match sw_names with | None -> res := [] | Some {blocks} -> - Ext_array.iter blocks (function - | {block_type = Some block_type} -> res := block_type :: !res - | {block_type = None} -> ())); + Ext_array.iter blocks (function {kind} -> + (match Ast_untagged_variants.block_kind_to_block_type kind with + | Some block_type -> res := block_type :: !res + | None -> ()))); !res let get_literal_cases (sw_names : Ast_untagged_variants.switch_names option) = @@ -183,6 +184,78 @@ let get_literal_cases (sw_names : Ast_untagged_variants.switch_names option) = | {name; tag_type = None} -> res := String name :: !res)); !res +let has_tagged_primitive_catchall + (sw_names : Ast_untagged_variants.switch_names option) : bool = + match sw_names with + | None -> false + | Some {blocks} -> + Array.exists + (fun {Ast_untagged_variants.kind} -> + match kind with + | Tagged_primitive_catchall _ -> true + | Tagged_block | Untagged_block _ -> false) + blocks + +let has_tagged_block (sw_names : Ast_untagged_variants.switch_names option) : + bool = + match sw_names with + | None -> false + | Some {blocks} -> + Array.exists + (fun {Ast_untagged_variants.kind} -> + match kind with + | Tagged_block -> true + | Tagged_primitive_catchall _ | Untagged_block _ -> false) + blocks + +let block_discriminant_expr ~sw_names ~tag_name (e : E.t) : E.t = + if has_tagged_primitive_catchall sw_names || has_tagged_block sw_names then + E.tag ~name:tag_name e + else e + +let get_block_literal_cases + (sw_names : Ast_untagged_variants.switch_names option) : + Ast_untagged_variants.tag_type list = + match sw_names with + | None -> [] + | Some {blocks; _} -> + let acc = ref [] in + Ext_array.iter blocks (function + | {kind = Tagged_block; tag} -> ( + match tag.tag_type with + | Some t -> acc := t :: !acc + | None -> acc := String tag.name :: !acc) + | _ -> ()); + !acc + +let split_sw_blocks_by_catchall sw_blocks get_block = + List.fold_right + (fun ((i, _) as case) (literals, catchalls) -> + match get_block i with + | Some {Ast_untagged_variants.kind = Tagged_block} -> + (case :: literals, catchalls) + | Some {kind = Tagged_primitive_catchall _ | Untagged_block _} -> + (literals, case :: catchalls) + | None -> (literals, catchalls)) + sw_blocks ([], []) + +let all_literal_cases_with_block_tags + (sw_names : Ast_untagged_variants.switch_names option) : + Ast_untagged_variants.tag_type list = + match sw_names with + | None -> [] + | Some {blocks; _} as names -> + if has_tagged_primitive_catchall names then ( + let acc = ref (get_literal_cases names) in + Ext_array.iter blocks (function + | {Ast_untagged_variants.kind = Tagged_block; tag} -> ( + match tag.tag_type with + | Some t -> acc := t :: !acc + | None -> acc := String tag.name :: !acc) + | _ -> ()); + !acc) + else get_literal_cases names + let has_null_undefined_other (sw_names : Ast_untagged_variants.switch_names option) = let null, undefined, other = (ref false, ref false, ref false) in @@ -641,12 +714,12 @@ let compile output_prefix = table (Some []) and compile_cases ?(untagged = false) ?(has_null_case = false) ~cxt ~(switch_exp : E.t) ?(default = NonComplete) ?(get_tag = fun _ -> None) - ?(block_cases = []) cases : initialization = + ?(block_cases = []) ?(literal_cases = []) cases : initialization = match use_compile_literal_cases cases ~get_tag with | Some string_cases -> if untagged then - compile_untagged_cases ~cxt ~switch_exp ~block_cases ~default - ~has_null_case string_cases + compile_untagged_cases ~cxt ~switch_exp ~block_cases ~literal_cases + ~default ~has_null_case string_cases else compile_string_cases ~cxt ~switch_exp ~default string_cases | None -> cases @@ -690,40 +763,98 @@ let compile output_prefix = let get_const_tag i = get_const_tag i sw_names in let get_block i = get_block i sw_names in let block_cases = get_block_cases sw_names in + let literal_cases = all_literal_cases_with_block_tags sw_names in + let block_literal_cases = get_block_literal_cases sw_names in let get_block_tag i : Ast_untagged_variants.tag option = match get_block i with | None -> None - | Some {tag = {name}; block_type = Some block_type} -> - Some {name; tag_type = Some (Untagged block_type)} (* untagged block *) - | Some {block_type = None; tag} -> - (* tagged block *) - Some tag + | Some {tag = {name}; kind = Tagged_primitive_catchall primitive_catchall} + -> + Some + { + name; + tag_type = + Some + (Untagged + (Ast_untagged_variants.primitive_catchall_to_block_type + primitive_catchall)); + } + | Some {tag = {name}; kind = Untagged_block block_type} -> + Some {name; tag_type = Some (Untagged block_type)} + | Some {kind = Tagged_block; tag} -> Some tag in let tag_name = get_tag_name sw_names in - let untagged = block_cases <> [] in + let has_block_cases = block_cases <> [] in let compile_whole (cxt : Lam_compile_context.t) = match compile_lambda {cxt with continuation = NeedValue Not_tail} switch_arg with | {value = None; _} -> assert false | {block; value = Some e} -> ( + let compile_tagged_primitive_block_cases discr = + let sw_blocks_literal_only, sw_blocks_untagged_only = + split_sw_blocks_by_catchall sw_blocks get_block + in + match (sw_blocks_literal_only, sw_blocks_untagged_only) with + | [], _ -> + compile_cases ~untagged:true ~cxt ~switch_exp:discr ~block_cases + ~literal_cases:block_literal_cases ~default:sw_blocks_default + ~get_tag:get_block_tag sw_blocks_untagged_only + | _, [] -> + compile_cases ~cxt ~switch_exp:discr ~block_cases + ~literal_cases:block_literal_cases ~default:sw_blocks_default + ~get_tag:get_block_tag sw_blocks_literal_only + | _ -> + [ + S.if_ + (E.is_a_literal_case ~literal_cases:block_literal_cases + ~block_cases discr) + (compile_cases ~cxt ~switch_exp:discr ~block_cases + ~literal_cases:block_literal_cases ~default:sw_blocks_default + ~get_tag:get_block_tag sw_blocks_literal_only) + ~else_: + (compile_cases ~untagged:true ~cxt ~switch_exp:discr + ~block_cases ~literal_cases:block_literal_cases + ~default:sw_blocks_default ~get_tag:get_block_tag + sw_blocks_untagged_only); + ] + in + let is_object_payload_case (e : E.t) = + E.and_ + (E.string_equal (E.typeof e) (E.str "object")) + (E.not (E.is_null e)) + in block @ - if sw_consts_full && sw_consts = [] then - compile_cases ~block_cases ~untagged ~cxt - ~switch_exp:(if untagged then e else E.tag ~name:tag_name e) + if has_tagged_primitive_catchall sw_names then + let discr = E.tag ~name:tag_name e in + if sw_consts = [] then compile_tagged_primitive_block_cases discr + else + [ + S.if_ (is_object_payload_case e) + (compile_tagged_primitive_block_cases discr) + ~else_: + (compile_cases ~cxt ~switch_exp:e ~default:sw_num_default + ~get_tag:get_const_tag sw_consts); + ] + else if sw_consts_full && sw_consts = [] then + compile_cases ~block_cases ~literal_cases ~untagged:has_block_cases + ~cxt + ~switch_exp:(block_discriminant_expr ~sw_names ~tag_name e) ~default:sw_blocks_default ~get_tag:get_block_tag sw_blocks else if sw_blocks_full && sw_blocks = [] then - compile_cases ~cxt ~switch_exp:e ~block_cases ~default:sw_num_default - ~get_tag:get_const_tag sw_consts + compile_cases ~cxt ~switch_exp:e ~block_cases ~literal_cases + ~default:sw_num_default ~get_tag:get_const_tag sw_consts else (* [e] will be used twice *) let dispatch e = + let block_switch_exp = + block_discriminant_expr ~sw_names ~tag_name e + in let is_a_literal_case () = - if untagged then - E.is_a_literal_case - ~literal_cases:(get_literal_cases sw_names) - ~block_cases e + if has_block_cases then + let lit_cases = all_literal_cases_with_block_tags sw_names in + E.is_a_literal_case ~literal_cases:lit_cases ~block_cases e else E.is_int_tag ~has_null_undefined_other:(has_null_undefined_other sw_names) @@ -737,29 +868,27 @@ let compile output_prefix = | _ -> false in if - untagged + has_block_cases && List.length sw_consts = 0 && eq_default sw_num_default sw_blocks_default + && not (has_tagged_primitive_catchall sw_names) then - let literal_cases = get_literal_cases sw_names in let has_null_case = List.mem Ast_untagged_variants.Null literal_cases in - compile_cases ~untagged ~cxt - ~switch_exp:(if untagged then e else E.tag ~name:tag_name e) - ~block_cases ~has_null_case ~default:sw_blocks_default - ~get_tag:get_block_tag sw_blocks + compile_cases ~untagged:has_block_cases ~cxt ~literal_cases + ~switch_exp:block_switch_exp ~block_cases ~has_null_case + ~default:sw_blocks_default ~get_tag:get_block_tag sw_blocks else [ S.if_ (is_a_literal_case ()) - (compile_cases ~cxt ~switch_exp:e ~block_cases + (compile_cases ~cxt ~literal_cases ~switch_exp:e ~block_cases ~default:sw_num_default ~get_tag:get_const_tag sw_consts) ~else_: - (compile_cases ~untagged ~cxt - ~switch_exp: - (if untagged then e else E.tag ~name:tag_name e) - ~block_cases ~default:sw_blocks_default - ~get_tag:get_block_tag sw_blocks); + (compile_cases ~untagged:has_block_cases ~cxt ~literal_cases + ~switch_exp:block_switch_exp ~block_cases + ~default:sw_blocks_default ~get_tag:get_block_tag + sw_blocks); ] in match e.expression_desc with @@ -795,16 +924,16 @@ let compile output_prefix = S.string_switch ?default ?declaration e clauses) ~switch_exp ~default and compile_untagged_cases ~cxt ~switch_exp ~default ~block_cases - ~has_null_case cases = + ~literal_cases ~has_null_case cases = let mk_eq (i : Ast_untagged_variants.tag_type option) x j y = let check = match (i, j) with | Some tag_type, _ -> Ast_untagged_variants.DynamicChecks.add_runtime_type_check ~tag_type - ~has_null_case ~block_cases (Expr x) (Expr y) + ~has_null_case ~block_cases ~literal_cases (Expr x) (Expr y) | _, Some tag_type -> Ast_untagged_variants.DynamicChecks.add_runtime_type_check ~tag_type - ~has_null_case ~block_cases (Expr y) (Expr x) + ~has_null_case ~block_cases ~literal_cases (Expr y) (Expr x) | _ -> Ast_untagged_variants.DynamicChecks.( == ) (Expr x) (Expr y) in E.emit_check check diff --git a/compiler/core/lam_constant_convert.ml b/compiler/core/lam_constant_convert.ml index a5764124949..ff3655edca1 100644 --- a/compiler/core/lam_constant_convert.ml +++ b/compiler/core/lam_constant_convert.ml @@ -43,7 +43,7 @@ let rec convert_constant (const : Lambda.structured_constant) : Lam_constant.t = | Pt_shape_none -> Lam_constant.lam_none | Pt_assertfalse -> Const_int {i = Int32.of_int i; comment = Pt_assertfalse} | Pt_constructor {name; const; non_const; attrs} -> - let tag_type = Ast_untagged_variants.process_tag_type attrs in + let tag_type = Ast_untagged_variants.process_constructor_tag_type attrs in let i = match tag_type with | Some (Ast_untagged_variants.Int v) -> v diff --git a/compiler/frontend/bs_ast_invariant.ml b/compiler/frontend/bs_ast_invariant.ml index cbe5a4432ee..5bda5d88ee8 100644 --- a/compiler/frontend/bs_ast_invariant.ml +++ b/compiler/frontend/bs_ast_invariant.ml @@ -28,7 +28,8 @@ *) let is_bs_attribute txt = match txt with - | "as" | "config" | "ignore" | "int" | "optional" | "string" | "unwrap" -> + | "as" | "catch" | "config" | "ignore" | "int" | "optional" | "string" + | "unwrap" -> true | _ -> false @@ -110,8 +111,8 @@ let emit_external_warnings : iterator = constructor_declaration = (fun self ({pcd_name = {txt; loc}} as ctr) -> let _ = - Ast_untagged_variants.process_tag_type - ctr.pcd_attributes (* mark @as used in variant cases *) + Ast_untagged_variants.process_constructor_annotation + ctr.pcd_attributes (* mark @as/@catch used in variant cases *) in (match txt with | "false" | "true" | "()" -> diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index 06673db5ed0..6787d7ed9f3 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -62,10 +62,21 @@ type untagged_error = | ConstructorMoreThanOneArg of string type error = | InvalidVariantAsAnnotation + | InvalidVariantCatchAnnotation | Duplicated_bs_as + | DuplicatedVariantConstructorAnnotation | InvalidVariantTagAnnotation | InvalidUntaggedVariantDefinition of untagged_error | TagFieldNameConflict of string * string * string + | TaggedPrimitiveCatchAll_TaggedVariantRequired + | TaggedPrimitiveCatchAll_UnboxedVariantUnsupported + | TaggedPrimitiveCatchAll_AtMostOneNumber + | TaggedPrimitiveCatchAll_AtMostOneString + | TaggedPrimitiveCatchAll_OnNullaryConstructor of string + | TaggedPrimitiveCatchAll_InlineRecordRequired of string + | TaggedPrimitiveCatchAll_TooManyTagFields of string * string + | TaggedPrimitiveCatchAll_TagFieldOptional of string * string + | TaggedPrimitiveCatchAll_TagFieldWrongType of string * string * string exception Error of Location.t * error let report_error ppf = @@ -75,7 +86,13 @@ let report_error ppf = fprintf ppf "A variant case annotation @as(...) must be a string or integer, \ boolean, null, undefined" + | InvalidVariantCatchAnnotation -> + fprintf ppf + "A variant case annotation @catch(...) must be int, float, or string" | Duplicated_bs_as -> fprintf ppf "duplicate @as " + | DuplicatedVariantConstructorAnnotation -> + fprintf ppf + "duplicate constructor annotation, use only one of @as or @catch" | InvalidVariantTagAnnotation -> fprintf ppf "A variant tag annotation @tag(...) must be a string" | InvalidUntaggedVariantDefinition untagged_variant -> @@ -105,6 +122,44 @@ 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 + | TaggedPrimitiveCatchAll_TaggedVariantRequired -> + fprintf ppf + "Primitive catch-all @catch(int|float|string) requires an explicit \ + @tag(\"...\") annotation on the variant type" + | TaggedPrimitiveCatchAll_UnboxedVariantUnsupported -> + fprintf ppf + "Primitive catch-all @catch(int|float|string) is not allowed on @unboxed \ + variants" + | TaggedPrimitiveCatchAll_AtMostOneNumber -> + fprintf ppf + "At most one number catch-all (@catch(int|float)) is allowed per variant" + | TaggedPrimitiveCatchAll_AtMostOneString -> + fprintf ppf + "At most one string catch-all (@catch(string)) is allowed per variant" + | TaggedPrimitiveCatchAll_OnNullaryConstructor name -> + fprintf ppf + "Constructor \"%s\": primitive catch-all @catch(int|float|string) is not \ + allowed on nullary constructors" + name + | TaggedPrimitiveCatchAll_InlineRecordRequired name -> + fprintf ppf + "Constructor \"%s\": primitive catch-all requires an inline record \ + payload" + name + | TaggedPrimitiveCatchAll_TooManyTagFields (name, tag_name) -> + fprintf ppf + "Constructor \"%s\": inline record may expose the discriminant through \ + at most one field named \"%s\" (or @as(\"%s\"))" + name tag_name tag_name + | TaggedPrimitiveCatchAll_TagFieldOptional (name, field_name) -> + fprintf ppf + "Constructor \"%s\": field \"%s\" must not be optional for primitive \ + catch-all" + name field_name + | TaggedPrimitiveCatchAll_TagFieldWrongType (name, field_name, expected) -> + fprintf ppf + "Constructor \"%s\": field \"%s\" must have type %s (direct builtin)" name + field_name expected (* Type of the runtime representation of an untagged block (case with payoad) *) type block_type = @@ -118,6 +173,8 @@ type block_type = | ObjectType | UnknownType +type primitive_catchall = PrimitiveInt | PrimitiveFloat | PrimitiveString + let block_type_to_user_visible_string = function | IntType -> "int" | StringType -> "string" @@ -129,6 +186,15 @@ let block_type_to_user_visible_string = function | ObjectType -> "object" | UnknownType -> "unknown" +let primitive_catchall_to_block_type = function + | PrimitiveInt -> IntType + | PrimitiveFloat -> FloatType + | PrimitiveString -> StringType + +let primitive_catchall_to_bucket = function + | PrimitiveInt | PrimitiveFloat -> `Number + | PrimitiveString -> `String + (* Type of the runtime representation of a tag. Can be a literal (case with no payload), or a block (case with payload). @@ -144,9 +210,18 @@ type tag_type = | Undefined (* literal or tagged block *) | Untagged of block_type (* untagged block *) type tag = {name: string; tag_type: tag_type option} -type block = {tag: tag; tag_name: string option; block_type: block_type option} +type block_kind = + | Tagged_block + | Tagged_primitive_catchall of primitive_catchall + | Untagged_block of block_type + +type block = {tag: tag; tag_name: string option; kind: block_kind} type switch_names = {consts: tag array; blocks: block array} +type constructor_runtime_representation = + | Constructor_primitive_catchall of primitive_catchall + | Constructor_tag of tag_type option + let tag_type_to_user_visible_string = function | String _ -> "string" | Int _ -> "int" @@ -157,6 +232,12 @@ let tag_type_to_user_visible_string = function | Undefined -> "undefined" | Untagged block_type -> block_type_to_user_visible_string block_type +let block_kind_to_block_type = function + | Tagged_block -> None + | Tagged_primitive_catchall primitive_catchall -> + Some (primitive_catchall_to_block_type primitive_catchall) + | Untagged_block block_type -> Some block_type + let untagged = "unboxed" let block_type_can_be_undefined = function @@ -190,6 +271,53 @@ let extract_concrete_typedecl : let expand_head : (Env.t -> Types.type_expr -> Types.type_expr) ref = ref (Obj.magic ()) +type constructor_annotation = + | Tag of tag_type + | PrimitiveCatchAll of primitive_catchall + +let process_constructor_annotation (attrs : Parsetree.attributes) = + let st : constructor_annotation option ref = ref None in + Ext_list.iter attrs (fun (({txt; loc}, payload) as attr) -> + match txt with + | "as" -> + if !st = None then ( + (match Ast_payload.is_single_string payload with + | None -> () + | Some (s, _dec) -> st := Some (Tag (String s))); + (match Ast_payload.is_single_int payload with + | None -> () + | Some i -> st := Some (Tag (Int i))); + (match Ast_payload.is_single_float payload with + | None -> () + | Some f -> st := Some (Tag (Float f))); + (match Ast_payload.is_single_bigint payload with + | None -> () + | Some i -> st := Some (Tag (BigInt i))); + (match Ast_payload.is_single_bool payload with + | None -> () + | Some b -> st := Some (Tag (Bool b))); + (match Ast_payload.is_single_ident payload with + | None -> () + | Some (Lident "null") -> st := Some (Tag Null) + | Some (Lident "undefined") -> st := Some (Tag Undefined) + | Some _ -> raise (Error (loc, InvalidVariantAsAnnotation))); + if !st = None then raise (Error (loc, InvalidVariantAsAnnotation)) + else Used_attributes.mark_used_attribute attr) + else raise (Error (loc, DuplicatedVariantConstructorAnnotation)) + | "catch" -> + if !st = None then ( + (match Ast_payload.is_single_ident payload with + | Some (Lident "int") -> st := Some (PrimitiveCatchAll PrimitiveInt) + | Some (Lident "float") -> + st := Some (PrimitiveCatchAll PrimitiveFloat) + | Some (Lident "string") -> + st := Some (PrimitiveCatchAll PrimitiveString) + | Some _ | None -> raise (Error (loc, InvalidVariantCatchAnnotation))); + Used_attributes.mark_used_attribute attr) + else raise (Error (loc, DuplicatedVariantConstructorAnnotation)) + | _ -> ()); + !st + let process_tag_type (attrs : Parsetree.attributes) = let st : tag_type option ref = ref None in Ext_list.iter attrs (fun (({txt; loc}, payload) as attr) -> @@ -222,6 +350,24 @@ let process_tag_type (attrs : Parsetree.attributes) = | _ -> ()); !st +let process_constructor_tag_type (attrs : Parsetree.attributes) = + match process_constructor_annotation attrs with + | Some (Tag tag_type) -> Some tag_type + | Some (PrimitiveCatchAll _) | None -> None + +let process_primitive_catchall (attrs : Parsetree.attributes) = + match process_constructor_annotation attrs with + | Some (PrimitiveCatchAll primitive_catchall) -> Some primitive_catchall + | Some (Tag _) | None -> None + +let has_primitive_catchall (attrs : Parsetree.attributes) = + process_primitive_catchall attrs <> None + +let constructor_runtime_representation (attrs : Parsetree.attributes) = + match process_primitive_catchall attrs with + | Some primitive_catchall -> Constructor_primitive_catchall primitive_catchall + | None -> Constructor_tag (process_constructor_tag_type attrs) + let () = Location.register_error_of_exn (function | Error (loc, err) -> Some (Location.error_of_printer loc report_error err) @@ -300,28 +446,41 @@ let get_block_type_from_typ ~env (t : Types.type_expr) : block_type option = | {desc = Ttuple _} -> Some (InstanceType Array) | _ -> None) -let get_block_type ~env (cstr : Types.constructor_declaration) : - block_type option = +let get_block_kind ~env (cstr : Types.constructor_declaration) : + block_kind option = match (process_untagged cstr.cd_attributes, cstr.cd_args) with - | false, _ -> None + | false, _ -> ( + match process_primitive_catchall cstr.cd_attributes with + | None -> None + | Some primitive_catchall -> + Some (Tagged_primitive_catchall primitive_catchall)) | true, Cstr_tuple [t] when get_block_type_from_typ ~env t |> Option.is_some -> - get_block_type_from_typ ~env t - | true, Cstr_tuple [ty] -> ( + Option.map + (fun block_type -> Untagged_block block_type) + (get_block_type_from_typ ~env t) + | true, Cstr_tuple [ty] -> let default = Some UnknownType in - match !extract_concrete_typedecl env ty with - | _, _, {type_kind = Type_record (_, Record_unboxed _)} -> default - | _, _, {type_kind = Type_record (_, _)} -> Some ObjectType - | _ -> default - | exception _ -> default) + let block_type = + match !extract_concrete_typedecl env ty with + | _, _, {type_kind = Type_record (_, Record_unboxed _)} -> default + | _, _, {type_kind = Type_record (_, _)} -> Some ObjectType + | _ -> default + | exception _ -> default + in + Option.map (fun block_type -> Untagged_block block_type) block_type | true, Cstr_tuple (_ :: _ :: _) -> (* C(_, _) with at least 2 args is an object *) - Some ObjectType + Some (Untagged_block ObjectType) | true, Cstr_record _ -> (* inline record is an object *) - Some ObjectType + Some (Untagged_block ObjectType) | true, _ -> None (* TODO: add restrictions here *) +let get_block_type ~env (cstr : Types.constructor_declaration) : + block_type option = + Option.bind (get_block_kind ~env cstr) block_kind_to_block_type + let process_tag_name (attrs : Parsetree.attributes) = let st = ref None in Ext_list.iter attrs (fun ({txt; loc}, payload) -> @@ -422,7 +581,7 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) check_literal ~is_const:true ~loc literal); if is_untagged_def then Ext_list.rev_iter blocks (fun (loc, block) -> - match block.block_type with + match block_kind_to_block_type block.kind with | Some block_type -> (match block_type with | UnknownType -> incr unknown_types @@ -441,13 +600,15 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) | None -> ()) else Ext_list.rev_iter blocks (fun (loc, block) -> - check_literal ~is_const:false ~loc block.tag) + match block.kind with + | Tagged_block -> check_literal ~is_const:false ~loc block.tag + | Tagged_primitive_catchall _ | Untagged_block _ -> ()) let get_cstr_loc_tag (cstr : Types.constructor_declaration) = ( cstr.cd_loc, { name = Ident.name cstr.cd_id; - tag_type = process_tag_type cstr.cd_attributes; + tag_type = process_constructor_tag_type cstr.cd_attributes; } ) let constructor_declaration_from_constructor_description ~env @@ -466,7 +627,12 @@ let names_from_type_variant ?(is_untagged_def = false) ~env (cstrs : Types.constructor_declaration list) = let get_block (cstr : Types.constructor_declaration) : block = let tag = snd (get_cstr_loc_tag cstr) in - {tag; tag_name = get_tag_name cstr; block_type = get_block_type ~env cstr} + let kind = + match get_block_kind ~env cstr with + | Some kind -> kind + | None -> Tagged_block + in + {tag; tag_name = get_tag_name cstr; kind} in let consts, blocks = Ext_list.fold_left cstrs ([], []) (fun (consts, blocks) cstr -> @@ -485,31 +651,95 @@ let check_tag_field_conflicts (cstrs : Types.constructor_declaration list) = List.iter (fun (cstr : Types.constructor_declaration) -> let constructor_name = Ident.name cstr.cd_id in + let primitive_catchall = process_primitive_catchall cstr.cd_attributes in let effective_tag_name = - match process_tag_name cstr.cd_attributes with - | Some explicit_tag -> explicit_tag - | None -> constructor_name + match primitive_catchall with + | Some _ -> ( + match process_tag_name cstr.cd_attributes with + | Some explicit_tag -> explicit_tag + | None -> assert false) + | None -> ( + match process_tag_name cstr.cd_attributes with + | Some explicit_tag -> explicit_tag + | None -> constructor_name) + in + let effective_field_name (field : Types.label_declaration) = + let field_name = Ident.name field.ld_id in + match process_tag_type field.ld_attributes with + | Some (String as_name) -> as_name + | Some _ | None -> field_name in - match cstr.cd_args with - | Cstr_record fields -> + match (primitive_catchall, cstr.cd_args) with + | None, Cstr_record fields -> List.iter (fun (field : Types.label_declaration) -> let field_name = Ident.name field.ld_id in - let effective_field_name = - match process_tag_type field.ld_attributes with - | Some (String as_name) -> as_name - (* @as payload types other than string have no effect on record fields *) - | Some _ | None -> field_name - in - (* Check if effective field name conflicts with tag *) - if effective_field_name = effective_tag_name then + let runtime_field_name = effective_field_name field in + if runtime_field_name = effective_tag_name then raise (Error ( cstr.cd_loc, TagFieldNameConflict - (constructor_name, field_name, effective_field_name) ))) + (constructor_name, field_name, runtime_field_name) ))) fields - | _ -> ()) + | Some _, Cstr_tuple [] -> + raise + (Error + ( cstr.cd_loc, + TaggedPrimitiveCatchAll_OnNullaryConstructor constructor_name )) + | Some _, Cstr_record fields -> ( + let matching_fields = + List.filter + (fun field -> effective_field_name field = effective_tag_name) + fields + in + match matching_fields with + | [] -> () + | _ :: _ :: _ -> + raise + (Error + ( cstr.cd_loc, + TaggedPrimitiveCatchAll_TooManyTagFields + (constructor_name, effective_tag_name) )) + | [field] -> + let field_name = Ident.name field.ld_id in + if field.ld_optional then + raise + (Error + ( cstr.cd_loc, + TaggedPrimitiveCatchAll_TagFieldOptional + (constructor_name, field_name) )); + let expected, ok = + match primitive_catchall with + | Some PrimitiveInt -> ( + match field.ld_type.desc with + | Tconstr (path, _, _) when Path.same path Predef.path_int -> + ("int", true) + | _ -> ("int", false)) + | Some PrimitiveFloat -> ( + match field.ld_type.desc with + | Tconstr (path, _, _) when Path.same path Predef.path_float -> + ("float", true) + | _ -> ("float", false)) + | Some PrimitiveString -> ( + match field.ld_type.desc with + | Tconstr (path, _, _) when Path.same path Predef.path_string -> + ("string", true) + | _ -> ("string", false)) + | None -> assert false + in + if not ok then + raise + (Error + ( cstr.cd_loc, + TaggedPrimitiveCatchAll_TagFieldWrongType + (constructor_name, field_name, expected) ))) + | Some _, _ -> + raise + (Error + ( cstr.cd_loc, + TaggedPrimitiveCatchAll_InlineRecordRequired constructor_name )) + | None, _ -> ()) cstrs type well_formedness_check = { @@ -518,10 +748,45 @@ type well_formedness_check = { } let check_well_formed ~env {is_untagged_def; cstrs} = + let primitive_catchalls = + List.filter_map + (fun (cstr : Types.constructor_declaration) -> + match process_primitive_catchall cstr.cd_attributes with + | None -> None + | Some primitive_catchall -> + Some + ( cstr.cd_loc, + Ident.name cstr.cd_id, + primitive_catchall, + process_tag_name cstr.cd_attributes )) + cstrs + in + (match primitive_catchalls with + | [] -> () + | (loc, _, _, tag_name) :: _ -> + if is_untagged_def then + raise (Error (loc, TaggedPrimitiveCatchAll_UnboxedVariantUnsupported)); + if tag_name = None then + raise (Error (loc, TaggedPrimitiveCatchAll_TaggedVariantRequired))); check_tag_field_conflicts cstrs; + let has_number_catchall = ref false in + let has_string_catchall = ref false in + List.iter + (fun (loc, _, primitive_catchall, _) -> + match primitive_catchall_to_bucket primitive_catchall with + | `Number -> + if !has_number_catchall then + raise (Error (loc, TaggedPrimitiveCatchAll_AtMostOneNumber)); + has_number_catchall := true + | `String -> + if !has_string_catchall then + raise (Error (loc, TaggedPrimitiveCatchAll_AtMostOneString)); + has_string_catchall := true) + primitive_catchalls; ignore (names_from_type_variant ~env ~is_untagged_def cstrs) -let has_undefined_literal attrs = process_tag_type attrs = Some Undefined +let has_undefined_literal attrs = + process_constructor_tag_type attrs = Some Undefined let block_is_object ~env attrs = get_block_type ~env attrs = Some ObjectType @@ -675,8 +940,29 @@ module DynamicChecks = struct else (* (undefiled + other) || other *) typeof e != object_ + let literal_cases_for_block_type (block_type : block_type) + (literal_cases : tag_type list) = + Ext_list.filter literal_cases (function + | String _ -> block_type = StringType + | Int _ | Float _ -> block_type = IntType || block_type = FloatType + | BigInt _ -> block_type = BigintType + | Bool _ -> block_type = BooleanType + | _ -> false) + + let literal_case_expr y (literal_case : tag_type) = y == tag_type literal_case + + let not_one_of_the_literals y = function + | [] -> None + | literal_1 :: rest -> + let is_literal_1 = literal_case_expr y literal_1 in + let is_any_literal = + Ext_list.fold_right rest is_literal_1 (fun literal_n acc -> + literal_case_expr y literal_n ||| acc) + in + Some (not is_any_literal) + let add_runtime_type_check ~tag_type ~has_null_case - ~(block_cases : block_type list) x y = + ~(block_cases : block_type list) ~(literal_cases : tag_type list) x y = let instances = Ext_list.filter_map block_cases (function | InstanceType i -> Some i @@ -684,9 +970,17 @@ module DynamicChecks = struct in match tag_type with | Untagged - ( IntType | StringType | FloatType | BigintType | BooleanType - | FunctionType ) -> - typeof y == x + ((IntType | StringType | FloatType | BigintType | BooleanType) as + block_type) -> ( + let runtime_type_matches = typeof y == x in + match + literal_cases + |> literal_cases_for_block_type block_type + |> not_one_of_the_literals y + with + | Some not_a_literal -> runtime_type_matches &&& not_a_literal + | None -> runtime_type_matches) + | Untagged FunctionType -> typeof y == x | Untagged ObjectType -> let object_case = if has_null_case then typeof y == x &&& (y != nil) else typeof y == x diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml index 1805844fd96..a0409e51ebd 100644 --- a/compiler/ml/error_message_utils.ml +++ b/compiler/ml/error_message_utils.ml @@ -682,10 +682,16 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf (fun (cd : Types.constructor_declaration) -> let constructor_name = Ident.name cd.cd_id in let runtime_repr = - match Ast_untagged_variants.process_tag_type cd.cd_attributes with - | Some (String s) -> Some s (* @as("string_value") *) - | Some _ -> None (* @as with non-string values *) - | None -> Some constructor_name (* No @as, use constructor name *) + match + Ast_untagged_variants.constructor_runtime_representation + cd.cd_attributes + with + | Ast_untagged_variants.Constructor_tag (Some (String s)) -> + Some s + | Ast_untagged_variants.Constructor_tag (Some _) -> None + | Ast_untagged_variants.Constructor_tag None -> + Some constructor_name + | Ast_untagged_variants.Constructor_primitive_catchall _ -> None in match runtime_repr with | Some repr -> Some (repr, constructor_name) diff --git a/compiler/ml/includecore.ml b/compiler/ml/includecore.ml index 05753d36c86..9df7a3f7ac0 100644 --- a/compiler/ml/includecore.ml +++ b/compiler/ml/includecore.ml @@ -254,18 +254,13 @@ and compare_variants ~loc env params1 params2 n in let r = if r <> [] then r - else - match Ast_untagged_variants.is_nullary_variant cd1.cd_args with - | true -> - let tag_type1 = - Ast_untagged_variants.process_tag_type cd1.cd_attributes - in - let tag_type2 = - Ast_untagged_variants.process_tag_type cd2.cd_attributes - in - if tag_type1 <> tag_type2 then [Variant_representation cd1.cd_id] - else [] - | false -> r + else if + Ast_untagged_variants.constructor_runtime_representation + cd1.cd_attributes + = Ast_untagged_variants.constructor_runtime_representation + cd2.cd_attributes + then [] + else [Variant_representation cd1.cd_id] in if r <> [] then r else compare_variants ~loc env params1 params2 (n + 1) rem1 rem2) diff --git a/compiler/ml/printtyp.ml b/compiler/ml/printtyp.ml index 9cf370da441..0c60417a095 100644 --- a/compiler/ml/printtyp.ml +++ b/compiler/ml/printtyp.ml @@ -912,17 +912,30 @@ and tree_of_constructor ?printing_context cd = let name = Ident.name cd.cd_id in let nullary = Ast_untagged_variants.is_nullary_variant cd.cd_args in let repr = - if not nullary then None - else - match Ast_untagged_variants.process_tag_type cd.cd_attributes with - | Some Null -> Some "@as(null)" - | Some Undefined -> Some "@as(undefined)" - | Some (String s) -> Some (Printf.sprintf "@as(%S)" s) - | Some (Int i) -> Some (Printf.sprintf "@as(%d)" i) - | Some (Float f) -> Some (Printf.sprintf "@as(%s)" f) - | Some (Bool b) -> Some (Printf.sprintf "@as(%b)" b) - | Some (BigInt s) -> Some (Printf.sprintf "@as(%sn)" s) - | Some (Untagged _) (* should never happen *) | None -> None + match + Ast_untagged_variants.constructor_runtime_representation cd.cd_attributes + with + | Ast_untagged_variants.Constructor_primitive_catchall + Ast_untagged_variants.PrimitiveInt -> + Some "@catch(int)" + | Ast_untagged_variants.Constructor_primitive_catchall + Ast_untagged_variants.PrimitiveFloat -> + Some "@catch(float)" + | Ast_untagged_variants.Constructor_primitive_catchall + Ast_untagged_variants.PrimitiveString -> + Some "@catch(string)" + | Ast_untagged_variants.Constructor_tag tag_type -> ( + if not nullary then None + else + match tag_type with + | Some Null -> Some "@as(null)" + | Some Undefined -> Some "@as(undefined)" + | Some (String s) -> Some (Printf.sprintf "@as(%S)" s) + | Some (Int i) -> Some (Printf.sprintf "@as(%d)" i) + | Some (Float f) -> Some (Printf.sprintf "@as(%s)" f) + | Some (Bool b) -> Some (Printf.sprintf "@as(%b)" b) + | Some (BigInt s) -> Some (Printf.sprintf "@as(%sn)" s) + | Some (Untagged _) (* should never happen *) | None -> None) in let arg () = tree_of_constructor_arguments ?printing_context cd.cd_args in match cd.cd_res with diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index ee304dff7d7..d22886a4b26 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -57,6 +57,7 @@ type error = string * Longident.t * (Path.t * Path.t) * (Path.t * Path.t) list | Undefined_method of type_expr * string * string list option | Private_type of type_expr + | Pattern_only_constructor of Longident.t | Private_label of Longident.t * type_expr | Not_subtype of Ctype.type_pairs * Ctype.type_pairs * Ctype.subtype_context option @@ -3795,6 +3796,8 @@ and type_construct ~context env loc lid sarg ty_expected attrs = Env.mark_constructor Env.Positive env (Longident.last lid.txt) constr; Builtin_attributes.check_deprecated loc constr.cstr_attributes constr.cstr_name; + if Ast_untagged_variants.has_primitive_catchall constr.cstr_attributes then + raise (Error (loc, env, Pattern_only_constructor lid.txt)); let sargs = match sarg with | None -> [] @@ -4563,6 +4566,10 @@ let report_error env loc ppf error = "In this type, the locally bound module name %s escapes its scope" id | Private_type ty -> fprintf ppf "Cannot create values of the private type %a" type_expr ty + | Pattern_only_constructor lid -> + fprintf ppf + "Constructor %a is pattern-only and cannot be used as an expression" + longident lid | Private_label (lid, ty) -> fprintf ppf "Cannot assign field %a of the private type %a" longident lid type_expr ty diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli index eef17d05a84..cad0e88ec88 100644 --- a/compiler/ml/typecore.mli +++ b/compiler/ml/typecore.mli @@ -90,6 +90,7 @@ type error = string * Longident.t * (Path.t * Path.t) * (Path.t * Path.t) list | Undefined_method of type_expr * string * string list option | Private_type of type_expr + | Pattern_only_constructor of Longident.t | Private_label of Longident.t * type_expr | Not_subtype of Ctype.type_pairs * Ctype.type_pairs * Ctype.subtype_context option diff --git a/compiler/ml/variant_coercion.ml b/compiler/ml/variant_coercion.ml index b3599dce11f..c91133bc3d3 100644 --- a/compiler/ml/variant_coercion.ml +++ b/compiler/ml/variant_coercion.ml @@ -46,7 +46,9 @@ let variant_has_same_runtime_representation_as_target ~(target_path : Path.t) (* Helper function to check if a constructor has the same runtime representation as the target type *) let has_same_runtime_representation (c : Types.constructor_declaration) = let args = c.cd_args in - let as_payload = Ast_untagged_variants.process_tag_type c.cd_attributes in + let as_payload = + Ast_untagged_variants.process_constructor_tag_type c.cd_attributes + in match args with | Cstr_tuple [{desc = Tconstr (p, [], _)}] when unboxed -> @@ -152,11 +154,20 @@ let can_try_coerce_variant_to_primitive_opt p = let variant_representation_matches (c1_attrs : Parsetree.attributes) (c2_attrs : Parsetree.attributes) = match - ( Ast_untagged_variants.process_tag_type c1_attrs, - Ast_untagged_variants.process_tag_type c2_attrs ) + ( Ast_untagged_variants.constructor_runtime_representation c1_attrs, + Ast_untagged_variants.constructor_runtime_representation c2_attrs ) with - | None, None -> true - | Some s1, Some s2 when s1 = s2 -> true + | ( Ast_untagged_variants.Constructor_primitive_catchall p1, + Ast_untagged_variants.Constructor_primitive_catchall p2 ) + when p1 = p2 -> + true + | ( Ast_untagged_variants.Constructor_tag None, + Ast_untagged_variants.Constructor_tag None ) -> + true + | ( Ast_untagged_variants.Constructor_tag (Some s1), + Ast_untagged_variants.Constructor_tag (Some s2) ) + when s1 = s2 -> + true | _ -> false type variant_configuration_error = @@ -264,15 +275,18 @@ let can_coerce_polyvariant_to_variant ~row_fields ~variant_constructors |> List.exists (fun (c : Types.constructor_declaration) -> let constructor_name = Ident.name c.cd_id in match - Ast_untagged_variants.process_tag_type c.cd_attributes + Ast_untagged_variants.constructor_runtime_representation + c.cd_attributes with - | Some (String as_runtime_string) -> + | Ast_untagged_variants.Constructor_tag + (Some (String as_runtime_string)) -> (* `@as("")`, does the configured string match the polyvariant value? *) as_runtime_string = polyvariant_value - | Some _ -> + | Ast_untagged_variants.Constructor_tag (Some _) + | Ast_untagged_variants.Constructor_primitive_catchall _ -> (* Any other `@as` can't match since it's by definition not a string *) false - | None -> ( + | Ast_untagged_variants.Constructor_tag None -> ( (* No `@as` means the runtime representation will be the constructor name as a string. diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_number.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_number.res.expected new file mode 100644 index 00000000000..d6555109591 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_number.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_duplicate_number.res:4:3-34 + + 2 │ type t = + 3 │ | @catch(int) A({kind: int}) + 4 │ | @catch(float) B({kind: float}) + 5 │ + + At most one number catch-all (@catch(int|float)) is allowed per variant \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_string.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_string.res.expected new file mode 100644 index 00000000000..8b2bca40a54 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_duplicate_string.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_duplicate_string.res:4:3-36 + + 2 │ type t = + 3 │ | @catch(string) A({kind: string}) + 4 │ | @catch(string) B({kind: string}) + 5 │ + + At most one string catch-all (@catch(string)) is allowed per variant \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_inline_record_required.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_inline_record_required.res.expected new file mode 100644 index 00000000000..7ab5f8ed071 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_inline_record_required.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_inline_record_required.res:2:10-29 + + 1 │ @tag("kind") + 2 │ type t = | @catch(int) A(int) + 3 │ + + Constructor "A": primitive catch-all requires an inline record payload \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_missing_tag_annotation.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_missing_tag_annotation.res.expected new file mode 100644 index 00000000000..481d0ad29bf --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_missing_tag_annotation.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_missing_tag_annotation.res:3:3-52 + + 1 │ type t = + 2 │ | @as("one") One({thing: string}) + 3 │ | @catch(string) Other({kind: string, thing: int}) + 4 │ + + Primitive catch-all @catch(int|float|string) requires an explicit @tag("...") annotation on the variant type \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_on_nullary.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_on_nullary.res.expected new file mode 100644 index 00000000000..a012ed54764 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_on_nullary.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_on_nullary.res:2:10-24 + + 1 │ @tag("kind") + 2 │ type t = | @catch(int) A + 3 │ + + Constructor "A": primitive catch-all @catch(int|float|string) is not allowed on nullary constructors \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_on_unboxed.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_on_unboxed.res.expected new file mode 100644 index 00000000000..3b2907118d2 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_on_unboxed.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_on_unboxed.res:4:3-52 + + 2 │ type t = + 3 │ | @as("one") One({thing: string}) + 4 │ | @catch(string) Other({kind: string, thing: int}) + 5 │ + + Primitive catch-all @catch(int|float|string) is not allowed on @unboxed variants \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_pattern_only_expression.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_pattern_only_expression.res.expected new file mode 100644 index 00000000000..957c849418c --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_pattern_only_expression.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_pattern_only_expression.res:6:9-39 + + 4 │ | @catch(string) Other({kind: string, thing: int}) + 5 │ + 6 │ let x = Other({kind: "one", thing: 12}) + 7 │ + + Constructor Other is pattern-only and cannot be used as an expression \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_variant_coercion.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_variant_coercion.res.expected new file mode 100644 index 00000000000..3a97e7fd4d7 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_variant_coercion.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_variant_coercion.res:11:27-34 + + 9 │ } + 10 │ + 11 │ let f = (x: A.t): B.t => (x :> B.t) + 12 │ + + Type A.t is not a subtype of B.t \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitive_catchall_wrong_type.res.expected b/tests/build_tests/super_errors/expected/primitive_catchall_wrong_type.res.expected new file mode 100644 index 00000000000..fff2d60cb19 --- /dev/null +++ b/tests/build_tests/super_errors/expected/primitive_catchall_wrong_type.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/primitive_catchall_wrong_type.res:2:10-53 + + 1 │ @tag("kind") + 2 │ type t = | @catch(int) A({kind: float, body: string}) + 3 │ + + Constructor "A": field "kind" must have type int (direct builtin) \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/variant_payload_runtime_representation_mismatch.res.expected b/tests/build_tests/super_errors/expected/variant_payload_runtime_representation_mismatch.res.expected new file mode 100644 index 00000000000..9cb581dbeb1 --- /dev/null +++ b/tests/build_tests/super_errors/expected/variant_payload_runtime_representation_mismatch.res.expected @@ -0,0 +1,28 @@ + + We've found a bug for you! + /.../fixtures/variant_payload_runtime_representation_mismatch.res:6:15-9:1 + + 4 │ } + 5 │ + 6 │ module M: S = { + 7 │  @tag("kind") + 8 │  type t = | @as("right") Other({value: int}) + 9 │ } + 10 │ + + Signature mismatch: + Modules do not match: + { + type t = Other({value: int}) +} + is not included in + S + Type declarations do not match: + type t = Other({value: int}) + is not included in + type t = Other({value: int}) + /.../fixtures/variant_payload_runtime_representation_mismatch.res:3:3-44: + Expected declaration + /.../fixtures/variant_payload_runtime_representation_mismatch.res:8:3-45: + Actual declaration + The internal representations for case Other are not equal. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_number.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_number.res new file mode 100644 index 00000000000..2d97aa1c755 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_number.res @@ -0,0 +1,4 @@ +@tag("kind") +type t = + | @catch(int) A({kind: int}) + | @catch(float) B({kind: float}) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_string.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_string.res new file mode 100644 index 00000000000..89c4e52176a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_duplicate_string.res @@ -0,0 +1,4 @@ +@tag("kind") +type t = + | @catch(string) A({kind: string}) + | @catch(string) B({kind: string}) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_inline_record_required.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_inline_record_required.res new file mode 100644 index 00000000000..02d7c15bf05 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_inline_record_required.res @@ -0,0 +1,2 @@ +@tag("kind") +type t = | @catch(int) A(int) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_missing_tag_annotation.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_missing_tag_annotation.res new file mode 100644 index 00000000000..8f280a860bc --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_missing_tag_annotation.res @@ -0,0 +1,3 @@ +type t = + | @as("one") One({thing: string}) + | @catch(string) Other({kind: string, thing: int}) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_on_nullary.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_on_nullary.res new file mode 100644 index 00000000000..49739035cc4 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_on_nullary.res @@ -0,0 +1,2 @@ +@tag("kind") +type t = | @catch(int) A diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_on_unboxed.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_on_unboxed.res new file mode 100644 index 00000000000..7dd51677fc7 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_on_unboxed.res @@ -0,0 +1,4 @@ +@unboxed @tag("kind") +type t = + | @as("one") One({thing: string}) + | @catch(string) Other({kind: string, thing: int}) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_pattern_only_expression.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_pattern_only_expression.res new file mode 100644 index 00000000000..4537771ef3f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_pattern_only_expression.res @@ -0,0 +1,6 @@ +@tag("kind") +type t = + | @as("one") One({thing: string}) + | @catch(string) Other({kind: string, thing: int}) + +let x = Other({kind: "one", thing: 12}) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_variant_coercion.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_variant_coercion.res new file mode 100644 index 00000000000..22e66607c3a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_variant_coercion.res @@ -0,0 +1,11 @@ +module A = { + @tag("kind") + type t = | @catch(string) Other({kind: string, value: int}) +} + +module B = { + @tag("kind") + type t = Other({value: int}) +} + +let f = (x: A.t): B.t => (x :> B.t) diff --git a/tests/build_tests/super_errors/fixtures/primitive_catchall_wrong_type.res b/tests/build_tests/super_errors/fixtures/primitive_catchall_wrong_type.res new file mode 100644 index 00000000000..0d82927876f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/primitive_catchall_wrong_type.res @@ -0,0 +1,2 @@ +@tag("kind") +type t = | @catch(int) A({kind: float, body: string}) diff --git a/tests/build_tests/super_errors/fixtures/variant_payload_runtime_representation_mismatch.res b/tests/build_tests/super_errors/fixtures/variant_payload_runtime_representation_mismatch.res new file mode 100644 index 00000000000..e90ecdee07e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/variant_payload_runtime_representation_mismatch.res @@ -0,0 +1,9 @@ +module type S = { + @tag("kind") + type t = | @as("left") Other({value: int}) +} + +module M: S = { + @tag("kind") + type t = | @as("right") Other({value: int}) +} diff --git a/tests/syntax_tests/data/idempotency/TaggedPrimitiveCatchAll.res b/tests/syntax_tests/data/idempotency/TaggedPrimitiveCatchAll.res new file mode 100644 index 00000000000..fbbcca856dd --- /dev/null +++ b/tests/syntax_tests/data/idempotency/TaggedPrimitiveCatchAll.res @@ -0,0 +1,17 @@ +@tag("kind") +type response = + | @as(202) Ok202({code: int}) + | @as(200) Ok200({code: int}) + | @catch(int) Other({kind: int, body: string}) + +let decode = (x: response) => + switch x { + | Ok202(r) => r.code + 1 + | Other(r) => r.kind + 2 + | Ok200(r) => r.code + 3 + } + +@tag("kind") +type noExposedDiscriminantField = + | @as("ok") Ok({value: int}) + | @catch(string) Other({value: int}) diff --git a/tests/tests/src/TaggedPrimitiveCatchAll.mjs b/tests/tests/src/TaggedPrimitiveCatchAll.mjs new file mode 100644 index 00000000000..c42e73feb35 --- /dev/null +++ b/tests/tests/src/TaggedPrimitiveCatchAll.mjs @@ -0,0 +1,262 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +function asResponse(x) { + return x; +} + +function decode(x) { + if (x.kind === 202 || x.kind === 200) { + if (x.kind === 202) { + return x.code + 1 | 0; + } else { + return x.code + 3 | 0; + } + } else { + return x.kind + 2 | 0; + } +} + +function decodePartial(x) { + if (x.kind === 202 || x.kind === 200) { + if (x.kind === 202) { + return x.code + 1 | 0; + } else { + return 999; + } + } else { + return x.kind + 2 | 0; + } +} + +let ok202 = ({kind: 202, code: 1}); + +let ok200 = ({kind: 200, code: 2}); + +let other404 = ({kind: 404, body: "x"}); + +function asT(x) { + return x; +} + +function classify(x) { + if (x.k === 200 || x.k === "ok") { + if (x.k === 200) { + return "num200"; + } else { + return "strok"; + } + } else if (typeof x.k === "number" && x.k !== 200) { + return "num:" + x.k.toString(); + } else { + return "str:" + x.k; + } +} + +function classifyPartial(x) { + if (x.k === 200 || x.k === "ok") { + return "lit"; + } else if (typeof x.k === "number" && x.k !== 200) { + return "num:" + x.k.toString(); + } else { + return "str:" + x.k; + } +} + +let num200 = ({k: 200, v: 1}); + +let strOk = ({k: "ok", msg: "m"}); + +let otherNum = ({k: 404, x: 1}); + +let otherStr = ({k: "else", s: "s"}); + +let NumAndStrCatchAll_num200Constructed = { + k: 200, + v: 1 +}; + +let NumAndStrCatchAll_strOkConstructed = { + k: "ok", + msg: "ok" +}; + +let NumAndStrCatchAll = { + asT: asT, + num200Constructed: NumAndStrCatchAll_num200Constructed, + strOkConstructed: NumAndStrCatchAll_strOkConstructed, + classify: classify, + classifyPartial: classifyPartial, + num200: num200, + strOk: strOk, + otherNum: otherNum, + otherStr: otherStr +}; + +function asT$1(x) { + return x; +} + +function sum(x) { + if (x.k !== 1 && x.k !== 3 && x.k !== 2) { + return x.k + x.y | 0; + } + switch (x.k) { + case 1 : + return x.x + 10 | 0; + case 2 : + return x.x + 20 | 0; + case 3 : + return x.x + 30 | 0; + } +} + +function sumPartial(x) { + if (typeof x.k === "number" && x.k !== 2 && x.k !== 1 && x.k !== 3) { + return x.k + x.y | 0; + } else { + return 0; + } +} + +let one = ({k: 1, x: 1}); + +let two = ({k: 2, x: 2}); + +let three = ({k: 3, x: 3}); + +let other = ({k: 42, y: 1}); + +let ManyNumLiteralsWithCatchAll_oneConstructed = { + k: 1, + x: 1 +}; + +let ManyNumLiteralsWithCatchAll_twoConstructed = { + k: 2, + x: 2 +}; + +let ManyNumLiteralsWithCatchAll_threeConstructed = { + k: 3, + x: 3 +}; + +let ManyNumLiteralsWithCatchAll = { + asT: asT$1, + oneConstructed: ManyNumLiteralsWithCatchAll_oneConstructed, + twoConstructed: ManyNumLiteralsWithCatchAll_twoConstructed, + threeConstructed: ManyNumLiteralsWithCatchAll_threeConstructed, + sum: sum, + sumPartial: sumPartial, + one: one, + two: two, + three: three, + other: other +}; + +function asT$2(x) { + return x; +} + +function classify$1(x) { + if (typeof x === "object" && x !== null) { + return 2; + } else { + return 1; + } +} + +let okResult = classify$1("ok"); + +let otherOkResult = classify$1(({kind: "ok", value: 1})); + +let NullaryStringAndCatchAll = { + asT: asT$2, + okConstructed: "ok", + classify: classify$1, + okResult: okResult, + otherOkResult: otherOkResult +}; + +function asT$3(x) { + return x; +} + +function classify$2(x) { + if (x.kind === "ok") { + return x.value; + } else { + return x.value + 10 | 0; + } +} + +let okResult$1 = classify$2(({kind: "ok", value: 1})); + +let otherResult = classify$2(({kind: "else", value: 2})); + +let NoExposedDiscriminantField_okConstructed = { + kind: "ok", + value: 1 +}; + +let NoExposedDiscriminantField = { + asT: asT$3, + okConstructed: NoExposedDiscriminantField_okConstructed, + classify: classify$2, + okResult: okResult$1, + otherResult: otherResult +}; + +function asT$4(x) { + return x; +} + +function classify$3(x) { + if (typeof x === "object" && x !== null) { + return 20; + } else { + return 10; + } +} + +let nilResult = classify$3(null); + +let otherResult$1 = classify$3(({kind: "else", value: 1})); + +let NullaryNullAndCatchAll_nilConstructed = null; + +let NullaryNullAndCatchAll = { + asT: asT$4, + nilConstructed: NullaryNullAndCatchAll_nilConstructed, + classify: classify$3, + nilResult: nilResult, + otherResult: otherResult$1 +}; + +let constructedOk202 = { + kind: 202, + code: 10 +}; + +let constructedOk200 = { + kind: 200, + code: 20 +}; + +export { + asResponse, + constructedOk202, + constructedOk200, + decode, + decodePartial, + ok202, + ok200, + other404, + NumAndStrCatchAll, + ManyNumLiteralsWithCatchAll, + NullaryStringAndCatchAll, + NoExposedDiscriminantField, + NullaryNullAndCatchAll, +} +/* okResult Not a pure module */ diff --git a/tests/tests/src/TaggedPrimitiveCatchAll.res b/tests/tests/src/TaggedPrimitiveCatchAll.res new file mode 100644 index 00000000000..a96df8a5989 --- /dev/null +++ b/tests/tests/src/TaggedPrimitiveCatchAll.res @@ -0,0 +1,150 @@ +@tag("kind") +type response = + | @as(202) Ok202({code: int}) + | @as(200) Ok200({code: int}) + | @catch(int) Other({kind: int, body: string}) + +let asResponse = (x: 'a): response => Obj.magic(x) +let constructedOk202 = Ok202({code: 10}) +let constructedOk200 = Ok200({code: 20}) + +let decode = (x: response) => + switch x { + | Ok202(r) => r.code + 1 + | Other(r) => r.kind + 2 + | Ok200(r) => r.code + 3 + } + +let decodePartial = (x: response) => + switch x { + | Other(r) => r.kind + 2 + | Ok202(r) => r.code + 1 + | _ => 999 + } + +let ok202 = asResponse(%raw(`({kind: 202, code: 1})`)) +let ok200 = asResponse(%raw(`({kind: 200, code: 2})`)) +let other404 = asResponse(%raw(`({kind: 404, body: "x"})`)) + +module NumAndStrCatchAll = { + @tag("k") + type t = + | @as(200) Num200({v: int}) + | @as("ok") StrOk({msg: string}) + | @catch(int) OtherNum({@as("k") tag: int, x: int}) + | @catch(string) OtherStr({@as("k") tag: string, s: string}) + + let asT = (x: 'a): t => Obj.magic(x) + let num200Constructed = Num200({v: 1}) + let strOkConstructed = StrOk({msg: "ok"}) + + let classify = (x: t): string => + switch x { + | Num200(_) => "num200" + | StrOk(_) => "strok" + | OtherNum(r) => "num:" ++ Int.toString(r.tag) + | OtherStr(r) => "str:" ++ r.tag + } + + let classifyPartial = (x: t): string => + switch x { + | OtherNum(r) => "num:" ++ Int.toString(r.tag) + | OtherStr(r) => "str:" ++ r.tag + | _ => "lit" + } + + let num200 = asT(%raw(`({k: 200, v: 1})`)) + let strOk = asT(%raw(`({k: "ok", msg: "m"})`)) + let otherNum = asT(%raw(`({k: 404, x: 1})`)) + let otherStr = asT(%raw(`({k: "else", s: "s"})`)) +} + +module ManyNumLiteralsWithCatchAll = { + @tag("k") + type t = + | @as(1) One({x: int}) + | @as(2) Two({x: int}) + | @as(3) Three({x: int}) + | @catch(int) Other({k: int, y: int}) + + let asT = (x: 'a): t => Obj.magic(x) + let oneConstructed = One({x: 1}) + let twoConstructed = Two({x: 2}) + let threeConstructed = Three({x: 3}) + + let sum = (x: t): int => + switch x { + | One(r) => r.x + 10 + | Two(r) => r.x + 20 + | Three(r) => r.x + 30 + | Other(r) => r.k + r.y + } + + let sumPartial = (x: t): int => + switch x { + | Other(r) => r.k + r.y + | _ => 0 + } + + let one = asT(%raw(`({k: 1, x: 1})`)) + let two = asT(%raw(`({k: 2, x: 2})`)) + let three = asT(%raw(`({k: 3, x: 3})`)) + let other = asT(%raw(`({k: 42, y: 1})`)) +} + +module NullaryStringAndCatchAll = { + @tag("kind") + type t = + | @as("ok") Ok + | @catch(string) Other({kind: string, value: int}) + + let asT = (x: 'a): t => Obj.magic(x) + let okConstructed = Ok + + let classify = (x: t): int => + switch x { + | Ok => 1 + | Other(_) => 2 + } + + let okResult = classify(asT(%raw(`"ok"`))) + let otherOkResult = classify(asT(%raw(`({kind: "ok", value: 1})`))) +} + +module NoExposedDiscriminantField = { + @tag("kind") + type t = + | @as("ok") Ok({value: int}) + | @catch(string) Other({value: int}) + + let asT = (x: 'a): t => Obj.magic(x) + let okConstructed = Ok({value: 1}) + + let classify = (x: t): int => + switch x { + | Ok(r) => r.value + | Other(r) => r.value + 10 + } + + let okResult = classify(asT(%raw(`({kind: "ok", value: 1})`))) + let otherResult = classify(asT(%raw(`({kind: "else", value: 2})`))) +} + +module NullaryNullAndCatchAll = { + @tag("kind") + type t = + | @as(null) Nil + | @catch(string) Other({kind: string, value: int}) + + let asT = (x: 'a): t => Obj.magic(x) + let nilConstructed = Nil + + let classify = (x: t): int => + switch x { + | Nil => 10 + | Other(_) => 20 + } + + let nilResult = classify(asT(%raw(`null`))) + let otherResult = classify(asT(%raw(`({kind: "else", value: 1})`))) +}