From aed04a9bfbf839161ea4ae569f02b116e51de643 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Wed, 20 May 2026 14:12:53 +0200 Subject: [PATCH 01/17] tests: add compiler error variant catalog A single reference doc enumerating every error variant the compiler can emit, with each entry mapped to either a fixture, a reason the variant is unreachable, or an open TODO. --- AGENTS.md | 20 +- tests/ERROR_VARIANTS.md | 530 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 tests/ERROR_VARIANTS.md diff --git a/AGENTS.md b/AGENTS.md index 4408a02822..838b5325cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,7 +168,25 @@ tests/ 2. **Integration tests** (`tests/tests/`) - End-to-end behavior 3. **Unit tests** (`tests/ounit_tests/`) - Compiler functions 4. **Build tests** (`tests/build_tests/`) - Error cases and edge cases -5. **Type tests** (`tests/build_tests/super_errors/`) - Type checking behavior +5. **Type tests** (`tests/build_tests/super_errors/`) - Single-file type checking errors +6. **Multi-file error tests** (`tests/build_tests/super_errors_multi/`) - Cross-module errors that need separate `.res` / `.resi` files + +#### Error variant catalog + +[`tests/ERROR_VARIANTS.md`](tests/ERROR_VARIANTS.md) is a per-module +catalog of every error and warning variant the compiler can emit, with +each entry mapped to a fixture (or a documented reason it's unreachable). + +**When adding or removing an error variant**, also update the catalog: + +1. Add (or remove) the row in the relevant module section. +2. Set the status (`✓` covered / `⚠` unreachable / `☐` TODO). +3. If covered, link the fixture path; if unreachable, note the reason. + +**When adding or removing a fixture**, update the corresponding row's +`Fixture` and status columns so the catalog stays in sync with the test +suite. The catalog is the primary tool for finding coverage gaps and +dead-code removal candidates; stale entries make both jobs harder. ## Build Commands & Development diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md new file mode 100644 index 0000000000..213cb0d617 --- /dev/null +++ b/tests/ERROR_VARIANTS.md @@ -0,0 +1,530 @@ +# Compiler error variant catalog + +A per-module table of every named error and warning variant the compiler +declares, tagged with whether a test fixture currently exercises it. + +The catalog has two practical uses: + +1. **Coverage expansion** — find rows tagged `☐` (reachable but no + fixture) and add a `super_errors` / `super_errors_multi` fixture for + each. The Notes column points at the trigger site and any required + AST shape. +2. **Dead-code removal** — rows tagged `⚠` are variants whose trigger + site is unreachable in the current parser / compiler, with a named + blocker. They can be deleted in a follow-up PR. The "Confirmed dead" + summary at the bottom groups them by reason. + +## Status legend + +| Symbol | Meaning | +|---|---| +| ✓ | Covered by at least one fixture under `tests/build_tests/super_errors/` or `tests/build_tests/super_errors_multi/`. | +| ⚠ | **Verified** unreachable: the trigger site has a specific blocker named in source (an exception declared but never raised, an AST node the parser doesn't construct, a guard that's always false). Candidate for dead-code removal. | +| ☐ | Reachable but no fixture yet; would be valuable to add. | +| ? | Trigger site is live but reachability from regular ReScript source isn't confirmed. Distinct from ⚠: a `?` means "I couldn't find a fixture that reaches it" rather than "the path is provably blocked". | + +The "Confirmed dead" summary section at the bottom only includes ⚠. + +## Scope + +This catalog enumerates **named error variants** — constructors of +`type error = …` declarations across `compiler/`. The compiler also has +~94 `Location.raise_errorf` and similar inline calls that produce +user-facing errors without a named variant. Those are **not** catalogued +here because there's no constructor to track. If someone wants to add +coverage for one of those messages, the first step is refactoring it +into a named variant. + +## Fixture paths + +- Single-file fixtures live in [`build_tests/super_errors/fixtures/`](build_tests/super_errors/fixtures/) with expected output in [`build_tests/super_errors/expected/`](build_tests/super_errors/expected/). +- Multi-file fixtures live in [`build_tests/super_errors_multi/fixtures//`](build_tests/super_errors_multi/fixtures/) with expected output in [`build_tests/super_errors_multi/expected/.expected`](build_tests/super_errors_multi/expected/). + +## How to update + +When you add or remove an error variant in `compiler/`, update the +corresponding row here too: + +1. Add (or remove) the row in the appropriate module section below. +2. Write a fixture if the variant is reachable. Use `super_errors/` for + single-file scenarios, `super_errors_multi/` for cross-file ones. +3. Snapshot with `node tests/build_tests/super_errors{,_multi}/input.js update`. +4. Set the status column accordingly. + +If a variant turns out to be unreachable, document the named blocker +here (so it gets ⚠ instead of `?`) and file a follow-up to delete the +dead code. + +--- + +## `compiler/ml/typecore.ml` + +The largest error type; covers expression / pattern type-checking. +Source: [typecore.ml:27](../compiler/ml/typecore.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Polymorphic_label` | ? | — | typecore.ml:1555. Triggers in record-pattern matching when a polymorphic field gets instantiated. Several `'a. 'a => 'a` record-field reproductions compiled cleanly; the trigger site is live but I couldn't find an AST that reaches it. | +| `Constructor_arity_mismatch` | ✓ | `constructor_arity_mismatch.res`, `constructor_arity_mismatch_pattern.res`, `arity_mismatch*.res` | Triggers in both expression (4028) and pattern (1426) paths. | +| `Label_mismatch` | ☐ | — | typecore.ml:3589. Record label type clash with explicit unify failure; often subsumed by `Pattern_type_clash` / `Expr_type_clash`. | +| `Pattern_type_clash` | ✓ | many `*_pattern_type_clash.res` etc. | Most-fired pattern error; covered through many fixtures but report-side sub-cases (option-vs-non-option trace, polyvariant context, etc.) remain partly untested. | +| `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | +| `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | +| `Orpat_vars` | ✓ | `orpat_vars_unbalanced.res` | | +| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Many trace-shape sub-cases (function-arg context, JSX, dict, async, polyvariant) covered piecemeal; sub-case coverage is the biggest open area for this variant. | +| `Apply_non_function` | ✓ | `apply_non_function.res` | | +| `Apply_wrong_label` | ✓ | `apply_wrong_label.res` | | +| `Label_multiply_defined` | ✓ | `label_multiply_defined_literal.res` | | +| `Labels_missing` | ✓ | `missing_label.res`, `missing_labels.res` | | +| `Label_not_mutable` | ✓ | `label_not_mutable.res` | | +| `Wrong_name` | ✓ | `wrong_name_record_field.res`, `Cross_record_extra_field` (multi) | | +| `Name_type_mismatch` | ✓ | `super_errors_multi/Cross_qualified_constructor_mismatch` | Cross-module constructor disambiguation. | +| `Undefined_method` | ✓ | `super_errors_multi/Cross_module_alias_dot_access`, `undefined_method` | | +| `Private_type` | ✓ | `private_type_construction.res` | | +| `Private_label` | ✓ | `private_label.res` | | +| `Not_subtype` | ✓ | `subtype_*.res`, `dict_show_no_coercion.res`, etc. | | +| `Too_many_arguments` | ✓ | `too_many_arguments.res`, `moreArguments*.res` | | +| `Abstract_wrong_label` | ? | — | typecore.ml:3502. Fires when a function literal's label doesn't match the expected arrow type. One attempted reproduction landed on `Expr_type_clash` but I didn't retest with care; trigger site is live. | +| `Scoping_let_module` | ✓ | `scoping_let_module.res` | | +| `Not_a_variant_type` | ☐ | — | typecore.ml:563/613/641. Triggers in polyvariant `Ppat_type` pattern (`...t` spread in pattern); needs `[%pat_type t]` or similar pattern AST. | +| `Incoherent_label_order` | ? | — | typecore.ml:3894. Triggers when labeled args reorder against an arrow type that contains the label but not at the current position. Couldn't construct a reproduction that didn't hit `Apply_wrong_label` first. | +| `Less_general` | ✓ | `less_general_universal.res` | | +| `Modules_not_allowed` | ✓ | `super_errors_multi/Modules_not_allowed_toplevel` | Toplevel `let module(M) = …` pattern with `allow_modules=false`. | +| `Cannot_infer_signature` | ✓ | `cannot_infer_signature.res` | | +| `Not_a_packed_module` | ✓ | `not_a_packed_module.res` | | +| `Recursive_local_constraint` | ⚠ | — | typecore.ml:369. Routed via `Unification_recursive_abbrev` in `ctype.ml`, which is raised only when `ctype.ml`'s `Recursive_abbrev` exception fires. **`Recursive_abbrev` is defined (ctype.ml:110, ctype.mli:61) but never raised anywhere in `compiler/`.** Confirmed dead. | +| `Unexpected_existential` | ✓ | `super_errors_multi/Unexpected_existential_in_let` | Destructuring GADT constructor with existential in toplevel `let`. | +| `Unqualified_gadt_pattern` | ✓ | `super_errors_multi/Cross_gadt_pattern` | Only reachable via cross-module GADT disambiguation; in single-file matching the constructor would resolve before this check. | +| `Invalid_interval` | ⚠ | — | typecore.ml:1349. Triggered by `Ppat_interval` pattern. **Verified: `Ppat_interval` has no construction site in `compiler/syntax/src/res_core.ml`** — only printer and ast_debugger handle it. | +| `Invalid_for_loop_index` | ✓ | `invalid_for_loop_index.res` | | +| `Invalid_for_of_pattern` | ⚠ | — | typecore.ml:3120/3152. Verified: parser `normalize_for_of_pattern` (`res_core.ml:3841`) replaces non-var / non-`_` patterns with `Ppat_any` before the typer sees them. | +| `No_value_clauses` | ☐ | — | typecore.ml:2575. Triggers when a `switch` has only exception clauses; needs `try` / `catch` with no value branch. | +| `Exception_pattern_below_toplevel` | ✓ | `exception_pattern_below_toplevel.res` | | +| `Inlined_record_escape` | ✓ | `inline_record_escape.res` | | +| `Inlined_record_expected` | ✓ | `inlined_record_expected.res`, `super_errors_multi/Cross_inline_record_constructor` | | +| `Invalid_extension_constructor_payload` | ✓ | `invalid_extension_constructor_payload.res` | | +| `Not_an_extension_constructor` | ✓ | `not_an_extension_constructor.res` | | +| `Break_outside_loop` | ✓ | `break_outside_loop.res`, `break_in_nested_function.res` | | +| `Continue_outside_loop` | ✓ | `continue_outside_loop.res`, `continue_in_nested_function.res` | | +| `Literal_overflow` | ✓ | `intoverflow.res` | | +| `Unknown_literal` | ☐ | — | typecore.ml:279/283. **Confirmed reachable**: `let x = 0z` produces `Unknown modifier 'z' for literal 0z`. The lexer (`res_scanner.ml:302`) accepts any `g..z` / `G..Z` suffix; unknown ones fire here. | +| `Illegal_letrec_pat` | ✓ | `illegal_letrec_pat.res` | | +| `Empty_record_literal` | ☐ | — | typecore.ml:2747. **Confirmed reachable**: `let bad = {}` (no type annotation) produces `Empty record literal {} should be type annotated or used in a record context.`. With an annotation, `Labels_missing` fires first. | +| `Uncurried_arity_mismatch` | ✓ | `arity_mismatch3.res` etc. | | +| `Field_not_optional` | ✓ | `fieldNotOptional.res` | | +| `Type_params_not_supported` | ☐ | — | typecore.ml:635. Variant spread pattern (`| ...a`) where `a` has type params. Existing `variant_spread_type_parameters.res` covers the typedecl path; this is a separate pattern-level path. | +| `Field_access_on_dict_type` | ✓ | `field_access_on_dict_type.res` | | +| `Jsx_not_enabled` | ☐ | — | typecore.ml:218/3470. Fires when JSX expressions are used without `-bs-jsx N`. Reachable but the existing `super_errors` runner always passes `-bs-jsx 4`. | + +--- + +## `compiler/ml/typedecl.ml` + +Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Repeated_parameter` | ✓ | `repeated_type_parameter.res` | | +| `Duplicate_constructor` | ✓ | `duplicate_variant_constructor.res` | | +| `Duplicate_label` | ✓ | `duplicate_labels_error.res` | | +| `Object_spread_with_record_field` | ✓ | `object_spread_with_record_field.res` | | +| `Recursive_abbrev` | ✓ | `recursive_type_abbreviation.res`, `recursive_type.res` | | +| `Cycle_in_def` | ✓ | `recursive_type_abbrev_cycle.res` | | +| `Definition_mismatch` | ✓ | `definition_mismatch.res` | | +| `Constraint_failed` | ✓ | `constraint_failed.res` | | +| `Inconsistent_constraint` | ✓ | `inconsistent_constraint.res` | | +| `Type_clash` | ☐ | — | typedecl.ml:125. Manifest type doesn't unify with kind. | +| `Parameters_differ` | ? | — | typedecl.ml:988. Non-uniform recursive type abbreviation; ReScript variant recursion is accepted, and abbreviations cycle to `Cycle_in_def` first. Hard to construct a reproduction that lands here exactly. | +| `Null_arity_external` | ⚠ | — | typedecl.ml:1900. The guard requires `prim_arity = 0` and `prim_native_name` not having the magic 20-byte encoding (`\132\149...`) and `prim_name` not starting with `%` or `#`. The encoding gets applied to every concrete external by `Primitive.parse_declaration`, and empty `prim_name` is rejected earlier by `external_ffi_types.ml` with "Not a valid global name". No path through the parser reaches it. | +| `Unbound_type_var` | ✓ | `unbound_type_var.res` | | +| `Cannot_extend_private_type` | ✓ | `cannot_extend_private_type.res` | | +| `Not_extensible_type` | ✓ | `not_extensible_type.res` | | +| `Extension_mismatch` | ☐ | — | Cross-module extension declaration mismatch via `.resi`/`.res`. | +| `Rebind_wrong_type` | ☐ | — | typedecl.ml:1653. `exception X(int) = OtherWithStringArg`. ReScript **does** support exception rebinding (parser at `res_core.ml:6660` emits `Pext_rebind`). My prior claim that the syntax wasn't exposed was wrong. | +| `Rebind_mismatch` | ☐ | — | typedecl.ml:1681. Rebinding from a different extensible type. Same parser support as above. | +| `Rebind_private` | ☐ | — | typedecl.ml:1684. Rebinding a private constructor as public. Same parser support. | +| `Bad_variance` | ✓ | `bad_variance.res`, `bad_variance_contra.res` | | +| `Unavailable_type_constructor` | ☐ | — | typedecl.ml:778. Requires a type path findable at parse time but missing during constraint enforcement; only cross-unit scenarios. | +| `Bad_fixed_type` | ? | — | typedecl.ml:190/193. `set_fixed_row` runs when `is_fixed_type` returns true — requires an open object `{..f: t}` or open polyvariant `[> #A]` as `ptype_manifest`. Then if the expanded head isn't `Tvariant` / `Tobject` (line 190) or the row variable isn't `Tvar` (line 193), error. Reachable in principle via an alias chain that collapses the open row, but I haven't constructed one. | +| `Unbound_type_var_ext` | ✓ | `unbound_type_var_extension.res` | | +| `Varying_anonymous` | ? | — | typedecl.ml:1263. Requires anonymous constrained type params under specific variance; very obscure but trigger site is live. | +| `Val_in_structure` | ? | — | typedecl.ml:1887. Requires `pval_prim = []` for an external. Parser emits at least one string for any external; `[]` would only come from PPX or manual AST construction. Probably effectively dead. | +| `Invalid_attribute` | ✓ | `invalid_attribute_not_undefined.res` | | +| `Bad_immediate_attribute` | ✓ | `bad_immediate_attribute.res` | | +| `Bad_unboxed_attribute` | ✓ | `bad_unboxed_attribute_abstract.res`, `bad_unboxed_attribute_mutable.res`, `bad_unboxed_attribute_many_fields.res`, `bad_unboxed_attribute_extensible.res` | All 4 sub-cases covered. | +| `Boxed_and_unboxed` | ✓ | `boxed_and_unboxed.res` | | +| `Nonrec_gadt` | ✓ | `nonrec_gadt.res` | | +| `Variant_runtime_representation_mismatch` | ✓ | `variant_coercion_*.res` (many) | | +| `Variant_spread_fail` | ✓ | `variant_spread_*.res` (many), `variant_spread_non_variant.res` | | + +--- + +## `compiler/ml/typemod.ml` + +Module-level errors. Source: [typemod.ml:24](../compiler/ml/typemod.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Cannot_apply` | ✓ | `cannot_apply_non_functor.res` | | +| `Not_included` | ✓ | All `super_errors_multi/Iface_*` fixtures wrap to this via `compunit`. | | +| `Cannot_eliminate_dependency` | ☐ | — | typemod.ml:1335. Requires anonymous functor application whose result still mentions the bound module; couldn't engineer despite multiple attempts. May be effectively dead — every fixture's `nondep_supertype` succeeded with existential substitution. | +| `Signature_expected` | ☐ | — | typemod.ml:78, 1184. Extract-sig on non-signature module type. | +| `Structure_expected` | ✓ | `super_errors_multi/Smoke_unbound_module_reference` (indirect); also `open_functor.res` | | +| `With_no_component` | ✓ | `with_no_component.res` | | +| `With_mismatch` | ✓ | `with_mismatch.res` | | +| `With_makes_applicative_functor_ill_typed` | ☐ | — | typemod.ml:258. Requires applicative-functor constructions ReScript syntax doesn't expose. | +| `With_changes_module_alias` | ☐ | — | typemod.ml:240. Requires `with module = ...` substitution invalidating an aliased path. ReScript may not parse `with module`. | +| `With_cannot_remove_constrained_type` | ? | — | typemod.ml:443. Triggers when destructive substitution `with type X<'a> := T` is applied where the substituted type has constrained type params (non-`Tvar`). One attempted reproduction succeeded; haven't found a triggering shape. | +| `Repeated_name` | ✓ | `repeated_def_*.res` (multiple) | | +| `Non_generalizable` | ✓ | `non_generalizable.res` | | +| `Non_generalizable_module` | ☐ | — | typemod.ml:1023. Module value with non-closed type at sealing time; cross-file. | +| `Interface_not_compiled` | ✓ | `super_errors_multi/Iface_not_compiled` | | +| `Not_allowed_in_functor_body` | ✓ | `super_errors_multi/not_allowed_in_functor_body` (TODO: confirm path) | | +| `Not_a_packed_module` | ✓ | `not_a_packed_module.res` | | +| `Incomplete_packed_module` | ✓ | `incomplete_packed_module.res` | | +| `Scoping_pack` | ⚠ | — | typemod.ml:1717. Requires first-class module pack where a constraint type has a level mismatch; very contrived. | +| `Recursive_module_require_explicit_type` | ✓ | `recursive_module_require_explicit_type.res` | | +| `Apply_generative` | ✓ | `apply_generative.res` | | +| `Cannot_scrape_alias` | ☐ | — | typemod.ml:77, 83, 1347. Requires `Env.scrape_alias` returning `Mty_alias` (alias target's `.cmi` not loaded). Only multi-unit scenarios. | + +--- + +## `compiler/ml/typetexp.ml` + +Type-expression errors. Source: [typetexp.ml:28](../compiler/ml/typetexp.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Unbound_type_variable` | ✓ | (covered indirectly via many fixtures) | | +| `Unbound_type_constructor` | ✓ | `typetexp_unbound_type_constructor.res` | | +| `Unbound_type_constructor_2` | ? | — | typetexp.ml:475/619. Triggers in object / polyvariant inheritance where the inherited type's row variable is `Tvar` with a path. Hard to construct, but not provably dead. | +| `Type_arity_mismatch` | ✓ | `type_arity_mismatch.res` | | +| `Type_mismatch` | ☐ | — | typetexp.ml:368/373. Type-constructor application with `unify_param` failure (368) or `enforce_constraints` failure (373); mostly subsumed by `Constraint_failed`. | +| `Alias_type_mismatch` | ✓ | `typetexp_alias_type_mismatch.res` | | +| `Present_has_conjunction` | ? | — | typetexp.ml:452. Polyvariant tag with conjunction (`&`) typing path. ReScript's parser doesn't have a `&` polyvariant operator that I can find, but the AST `Rtag` constructor supports a conjunction list, so PPX-generated AST could reach it. | +| `Present_has_no_type` | ? | — | typetexp.ml:501. Same `Rtag`-with-conjunction family. | +| `Constructor_mismatch` | ✓ | `polyvariant_constructor_mismatch.res` | | +| `Not_a_variant` | ☐ | — | typetexp.ml:476. Polyvariant inheritance from non-variant. | +| `Variant_tags` | ⚠ | — | typetexp.ml:39. Raised at typecore.ml:342, 349, 367 via `Tags` exception from `ctype.ml`. **Verified: `exception Tags` is defined (ctype.ml:60) but never raised in `compiler/`.** Confirmed dead. | +| `Invalid_variable_name` | ✓ | `invalid_type_variable_name.res` | | +| `Cannot_quantify` | ? | — | typetexp.ml:540. Triggers in `Ptyp_poly` translation when a quantified variable becomes non-generic. Every value-level reproduction lands on `Less_general` first, but type-level constructions with constraints might still reach it. | +| `Multiple_constraints_on_type` | ✓ | `multiple_constraints_on_type.res` | | +| `Method_mismatch` | ✓ | `object_method_mismatch.res` | | +| `Unbound_value` | ✓ | `typetexp_unbound_value.res` | | +| `Unbound_constructor` | ✓ | `typetexp_unbound_constructor.res` | | +| `Unbound_label` | ✓ | `typetexp_unbound_label.res` | | +| `Unbound_module` | ✓ | `suggest_module_for_missing_identifier.res`, `super_errors_multi/Smoke_unbound_module_reference` | | +| `Unbound_modtype` | ✓ | `typetexp_unbound_modtype.res` | | +| `Ill_typed_functor_application` | ⚠ | — | typetexp.ml:102. In the `Longident.Lapply` branch. **Verified: parser has no construction site for `Longident.Lapply`** (no result in `res_core.ml`). Confirmed dead. | +| `Illegal_reference_to_recursive_module` | ☐ | — | typetexp.ml:75/114. Catches `Env.Recmodule` exception, raised when looking up a module currently being recursively defined (`#recmod#` placeholder, env.ml:1048). Reachable in principle via a recmodule whose signature references another recmodule member's type before sealing; couldn't construct a triggering fixture but trigger sites are live. | +| `Access_functor_as_structure` | ✓ | `access_functor_as_structure.res` | | +| `Apply_structure_as_functor` | ⚠ | — | typetexp.ml:93. In the `Longident.Lapply` branch. Same dead reason as `Ill_typed_functor_application`. | +| `Cannot_scrape_alias` | ☐ | — | typetexp.ml:86 (Ldot path, live), 95/101 (Lapply path, dead since `Lapply` isn't parsed). The Ldot trigger needs `Env.scrape_alias` to return `Mty_alias` — i.e. an alias whose target `.cmi` can't be loaded. Multi-unit only. | +| `Opened_object` | ✓ | `object_inherit_opened.res` | | +| `Not_an_object` | ✓ | `object_inherit_not_an_object.res` | | + +--- + +## `compiler/ml/includemod.ml` (symptom) + +Wrapper symptoms attached to inclusion failures. Source: [includemod.ml:23](../compiler/ml/includemod.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Missing_field` | ✓ | `super_errors_multi/Iface_missing_value` | | +| `Value_descriptions` | ✓ | `super_errors_multi/Iface_value_descriptions`, `super_errors_multi/Smoke_interface_mismatch` | | +| `Type_declarations` | ✓ | `super_errors_multi/Iface_type_decl_record`, `super_errors_multi/Iface_type_decl_variant`, `RecordInclusion.res` | | +| `Extension_constructors` | ✓ | `super_errors_multi/Iface_extension_constructors` | | +| `Module_types` | ✓ | `super_errors_multi/Iface_module_types` | | +| `Modtype_infos` | ✓ | `super_errors_multi/Iface_modtype_infos` | | +| `Modtype_permutation` | ✓ | `super_errors_multi/include_modtype_permutation` | | +| `Interface_mismatch` | ✓ | wrapper added to all `Iface_*` failures (line 476). | | +| `Unbound_modtype_path` | ☐ | — | includemod.ml:94. Requires module-type path comparison to fail; only triggers via destructive substitution paths ReScript doesn't expose. | +| `Unbound_module_path` | ☐ | — | includemod.ml:226/233. Alias comparison where `normalize_path` fails. Multi-unit scenarios only. | +| `Invalid_module_alias` | ☐ | — | includemod.ml:211. Requires both sides `Mty_alias` with one pointing to a functor argument. Functor-with-alias-sig fixtures hit `Module_types` instead. | + +--- + +## `compiler/ml/includecore.ml` (`type_mismatch`) + +Sub-symptoms produced during signature inclusion (rendered inside `Type_declarations`). +Source: [includecore.ml:159](../compiler/ml/includecore.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Arity` | ✓ | `definition_mismatch.res` | | +| `Privacy` | ✓ | `super_errors_multi/Iface_privacy_mismatch` | | +| `Kind` | ☐ | — | E.g. record vs variant mismatch between `.resi` and `.res`. | +| `Constraint` | ☐ | — | Type abbreviation constraint mismatch. | +| `Manifest` | ☐ | — | Manifest type differs. | +| `Variance` | ☐ | — | Variance annotations differ. | +| `Field_type` | ✓ | `super_errors_multi/Iface_type_decl_record` | | +| `Field_mutable` | ✓ | `super_errors_multi/Iface_field_mutable_mismatch` | | +| `Field_optional` | ✓ | `super_errors_multi/Iface_field_optional_mismatch` | | +| `Field_arity` | ☐ | — | Constructor with different argument count. | +| `Field_names` | ☐ | — | Record field names differ at position. | +| `Field_missing` | ✓ | `super_errors_multi/Iface_missing_value` (indirect) | | +| `Record_representation` | ☐ | — | Boxed-vs-unboxed record representation mismatch. | +| `Unboxed_representation` | ✓ | `super_errors_multi/Iface_unboxed_variant_mismatch` | | +| `Immediate` | ☐ | — | `@immediate` attribute mismatch. | +| `Tag_name` | ✓ | `super_errors_multi/Iface_tag_name_mismatch` | | +| `Variant_representation` | ✓ | `super_errors_multi/Iface_variant_representation_mismatch` | | + +--- + +## `compiler/frontend/bs_syntaxerr.ml` + +FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../compiler/frontend/bs_syntaxerr.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Unsupported_predicates` | ☐ | — | ast_attributes.ml:55, 69. **Reachable** via object type field with unknown `@get` / `@set` predicate keys (e.g. `{@get({weird: true}) "x": int}` on object type field). Called from `process_method_attributes_rev` → `process_getter_setter` for `Ptyp_object` fields. | +| `Conflict_bs_bs_this_bs_meth` | ☐ | — | bs_syntaxerr.ml:68. `@this` and `@meth` co-applied. `@this` **is** accepted (`let f = @this (self => self)` parses and goes through `to_method_callback`); needs constructing a case where the conflict check fires. | +| `Duplicated_bs_deriving` | ✓ | `duplicated_bs_deriving.res` | | +| `Conflict_attributes` | ✓ | `bs_conflict_attributes.res` | | +| `Expect_int_literal` | ✓ | `bs_expect_int_literal.res` | | +| `Expect_string_literal` | ✓ | `bs_expect_string_literal.res` | | +| `Expect_int_or_string_or_json_literal` | ☐ | — | ast_attributes.ml:268. **Reachable** via `_ [@as ]` in an external where the payload is neither int nor a delimited string (e.g. `@as(true)` or `@as(())`). Called from `refine_arg_type` in `ast_external_process.ml:78` for wildcard external arguments. | +| `Unhandled_poly_type` | ? | — | ast_core_type.ml:141. Triggers in `list_of_arrow` when an arrow chain contains a `Ptyp_poly`. The parser doesn't normally produce inline poly types inside arrows, but record fields can have polytypes that flow through these utilities. | +| `Invalid_underscore_type_in_external` | ? | — | ast_external_process.ml:107/132. Needs `_` in optional-label external position with no `@as`. Probably reachable in `@@obj` externals; not yet verified. | +| `Invalid_bs_string_type` | ✓ | `bs_invalid_bs_string_type.res` | | +| `Invalid_bs_int_type` | ✓ | `bs_invalid_bs_int_type.res` | | +| `Invalid_bs_unwrap_type` | ✓ | `bs_invalid_bs_unwrap_type.res` | | +| `Conflict_ffi_attribute` | ✓ | `conflicting_ffi_attributes.res` | | +| `Illegal_attribute` | ✓ | `bs_illegal_attribute_scope.res` | | +| `Not_supported_directive_in_bs_return` | ✓ | `bs_not_supported_directive_in_bs_return.res` | | +| `Expect_opt_in_bs_return_to_opt` | ✓ | `bs_expect_opt_in_bs_return_to_opt.res` | | +| `Misplaced_label_syntax` | ⚠ | — | bs_syntaxerr.ml:116. Only fires from `check_and_discard` in `ast_exp_apply.ml:49`, applied to the args of `->`, `#=`, `##` operators. The parser always emits those args as `Nolabel`. | +| `Optional_in_uncurried_bs_attribute` | ☐ | — | bs_syntaxerr.ml:112. Called from `ast_uncurry_gen.ml:34/40` for `@this` body args. `@this` **is** accepted on regular function literals (`let f = @this (self => self)` works); needs an `@this` function with an optional arg to trigger. | +| `Bs_this_simple_pattern` | ☐ | — | ast_uncurry_gen.ml:32. Same `@this` family — fires when the self pattern isn't a single variable. Reachable via `@this`-annotated function literal with a destructured self pattern. | +| `Experimental_feature_not_enabled` | ✓ | `let_unwrap_on_top_level_not_enabled.res` (and other let-unwrap variants) | Currently only `LetUnwrap` is checked. | +| `LetUnwrap_not_supported_in_position` | ✓ | `let_unwrap_on_top_level.res`, `let_unwrap_on_not_supported_variant.res` | | + +--- + +## `compiler/ml/ast_untagged_variants.ml` + +Untagged-variant validation errors. Source: [ast_untagged_variants.ml:52](../compiler/ml/ast_untagged_variants.ml). + +### `untagged_error` + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `OnlyOneUnknown` | ☐ | — | More than one constructor with `Unknown` type. | +| `AtMostOneObject` | ☐ | — | More than one object constructor. | +| `AtMostOneInstance` | ☐ | — | More than one constructor for a given JS instance. | +| `AtMostOneFunction` | ☐ | — | More than one function-typed constructor. | +| `AtMostOneString` | ✓ | `UntaggedNonUnary*.res` (some sub-cases) | | +| `AtMostOneNumber` | ☐ | — | | +| `AtMostOneBigint` | ☐ | — | | +| `AtMostOneBoolean` | ☐ | — | | +| `DuplicateLiteral` | ☐ | — | Same `@as` literal on multiple constructors. | +| `ConstructorMoreThanOneArg` | ☐ | — | Multi-arg constructor in untagged variant. | + +### `error` + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `InvalidVariantAsAnnotation` | ☐ | — | | +| `Duplicated_bs_as` | ☐ | — | Two `@as` attributes on same constructor. | +| `InvalidVariantTagAnnotation` | ☐ | — | | +| `InvalidUntaggedVariantDefinition` | ✓ | `UntaggedUnknown.res`, `UntaggedNonUnary*.res`, `UntaggedTupleAndArray.res`, `UntaggedImplIntf.res` | | +| `TagFieldNameConflict` | ☐ | — | | + +--- + +## `compiler/depends/bs_exception.ml` + +Build / dependency errors. Mostly need the `rescript build` runtime to fire — not reachable from raw `bsc`. Source: [bs_exception.ml:25](../compiler/depends/bs_exception.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Cmj_not_found` | ☐ | — | Missing `.cmj` from a dependent module. Needs `rescript build` harness. | +| `Js_not_found` | ✓ | implicitly — bypassed via `-bs-cmi-only` in `super_errors_multi` runner. Not a fixture, but the harness commit documents the workaround. | | +| `Bs_cyclic_depends` | ☐ | — | Cycle across compilation units; needs build-system harness. | +| `Bs_duplicated_module` | ☐ | — | Same module name in two source paths. | +| `Bs_duplicate_exports` | ☐ | — | Same export emitted twice; depends/build setup needed. | +| `Bs_package_not_found` | ☐ | — | `rescript.json`-referenced package not resolvable. | +| `Bs_main_not_exist` | ☐ | — | `rescript.json` `main` entry missing. | +| `Bs_invalid_path` | ☐ | — | `-I` / source path with invalid form. | +| `Missing_ml_dependency` | ☐ | — | Compile-time missing dependency. | +| `Dependency_script_module_dependent_not` | ☐ | — | `js_name_of_module_id.cppo.ml:122`. **Reachable** when a dependent module is in script mode (`Package_script`) but the current module is in package mode (`Package_found _`). Legacy script-vs-package interaction; needs `rescript.json` harness. | + +--- + +## `compiler/ml/env.ml` + +Environment / `.cmi`-consistency errors. Source: [env.ml:57](../compiler/ml/env.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Illegal_renaming` | ☐ | — | `.cmi` filename doesn't match module name; multi-unit scenario. | +| `Inconsistent_import` | ☐ | — | Two `.cmi` files disagree on a type's hash; needs synthetic build state. | +| `Missing_module` | ☐ | — | `.cmi` not findable when referenced; needs multi-file harness. | +| `Illegal_value_name` | ☐ | — | Reserved identifier name; very specific. | + +--- + +## `compiler/ml/cmi_format.ml` + +`.cmi` file format errors. Need binary-level manipulation to trigger. + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Not_an_interface` | ☐ | — | Pass an arbitrary file as `.cmi`. | +| `Wrong_version_interface` | ☐ | — | Mismatched compiler versions writing/reading. | +| `Corrupted_interface` | ☐ | — | Truncated or corrupted `.cmi`. | + +--- + +## `compiler/core/cmd_ast_exception.ml` + +PPX-runtime errors. Source: [cmd_ast_exception.ml:24](../compiler/core/cmd_ast_exception.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `CannotRun` | ☐ | — | PPX binary fails to execute. | +| `WrongMagic` | ☐ | — | PPX returns wrong AST magic number. | + +--- + +## Single-variant modules + +| Module | Variant | Status | Fixture | Notes | +|---|---|---|---|---| +| `compiler/ml/translcore.ml` | `Unknown_builtin_primitive` | ✓ | `unknown_builtin_primitive.res` | | +| `compiler/ml/translmod.ml` | `Fragile_pattern_in_toplevel` | ✓ | `fragile_pattern_toplevel.res` | | +| `compiler/ml/transl_recmodule.ml` | `Circular_dependency` | ✓ | `recmodule_circular_dependency.res` | | +| `compiler/ml/rec_check.ml` | `Illegal_letrec_expr` | ✓ | `illegal_letrec_expr.res` | | +| `compiler/ml/syntaxerr.ml` | `Variable_in_scope` | ☐ | — | Raised by `Ast_helper.Typ.varify_constructors` (`ast_helper.ml:84`, `ast_helper0.ml:74`) when a type variable shadows an outer one during alias expansion. Reachable. | +| `compiler/ml/cmt_format.cppo.ml` | `Not_a_typedtree` | ☐ | — | cmt_format.cppo.ml:147. Fires when reading a `.cmt` file that doesn't contain a typed tree. Needs binary `.cmt` manipulation. | +| `compiler/ext/bsc_args.ml` | `Unknown` | ☐ | — | bsc_args.ml:45. Unknown CLI flag passed to `bsc`. Reachable via `bsc --bogus`. | +| `compiler/ext/bsc_args.ml` | `Missing` | ☐ | — | Required CLI flag argument missing (e.g. `bsc -o` with no following filename). | + +--- + +## `compiler/frontend/ast_utf8_string.ml` (dead family) + +Source: [ast_utf8_string.ml:25](../compiler/frontend/ast_utf8_string.ml). All variants here are reached only via the legacy `{j|…|j}` delimiter, which the modern ReScript parser doesn't emit. Backtick template strings skip the transform entirely. + +| Variant | Status | +|---|---| +| `Invalid_code_point` | ⚠ Dead | +| `Unterminated_backslash` | ⚠ Dead | +| `Invalid_hex_escape` | ⚠ Dead | +| `Invalid_unicode_escape` | ⚠ Dead | +| `Invalid_unicode_codepoint_escape` | ⚠ Dead | + +## `compiler/frontend/ast_utf8_string_interp.ml` + +Source: [ast_utf8_string_interp.ml:25](../compiler/frontend/ast_utf8_string_interp.ml). + +| Variant | Status | Fixture | Notes | +|---|---|---|---| +| `Invalid_code_point` | ☐ | — | | +| `Unterminated_backslash` | ☐ | — | | +| `Invalid_escape_code` | ☐ | — | | +| `Invalid_hex_escape` | ☐ | — | | +| `Invalid_unicode_escape` | ☐ | — | | +| `Unterminated_variable` | ☐ | — | | +| `Unmatched_paren` | ☐ | — | | +| `Invalid_syntax_of_var` | ☐ | — | | + +--- + +## Confirmed dead variants — candidates for removal + +Only variants with a concrete, source-level reason are listed. Each row +has been re-verified against the source as of this audit. Variants marked +`?` in the tables above are **not** included here — those may turn out to +be live and just hard to reproduce. + +**Verified dead by missing raise / construction site:** + +- `typecore.Variant_tags`, `typetexp.Variant_tags` — relayed via the + `Tags` exception which is declared in `ctype.ml:60` / `ctype.mli:57` + but **never raised** in `compiler/`. +- `typecore.Recursive_local_constraint` — relayed via + `Unification_recursive_abbrev`, raised only from the `Recursive_abbrev` + exception which is declared (`ctype.ml:110`, `ctype.mli:61`) but + **never raised**. +- `typecore.Invalid_interval` — needs `Ppat_interval`; **no construction + site** for that AST node in `compiler/syntax/src/`. +- `typecore.Invalid_for_of_pattern` — parser's + `normalize_for_of_pattern` (`res_core.ml:3841`) replaces every non-var, + non-`_` pattern with `Ppat_any` before the typer runs. + +**Verified dead because parser doesn't produce required AST shape:** + +- `typetexp.Ill_typed_functor_application`, + `typetexp.Apply_structure_as_functor` — in the + `Longident.Lapply` branch; `Lapply` has no construction site in + the parser (`res_core.ml`). +- `bs_syntaxerr.Misplaced_label_syntax` — fires for labeled args to + `->`/`#=`/`##` operators; the parser always emits those with + `Nolabel`. +- `typedecl.Null_arity_external` — primitives parsed by + `Primitive.parse_declaration` always get the magic 20-byte + `prim_native_name` encoding, which bypasses the trigger; empty + `prim_name` is rejected earlier with "Not a valid global name". +- `ast_utf8_string.*` (Invalid_code_point, Unterminated_backslash, + Invalid_hex_escape, Invalid_unicode_escape, + Invalid_unicode_codepoint_escape) — the scanner + (`res_scanner.ml:350-417`) already validates escape sequences and + unicode code points; the transform never sees a string that would + fail its own re-validation. + +**Probably dead but not formally verified** (`?` in tables above; needs +deeper analysis before removal): `Polymorphic_label`, +`Abstract_wrong_label`, `Incoherent_label_order`, `Parameters_differ`, +`Bad_fixed_type`, `Varying_anonymous`, `Val_in_structure`, +`Unbound_type_constructor_2`, `Cannot_quantify`, +`Present_has_conjunction`, `Present_has_no_type`, +`With_cannot_remove_constrained_type`, `Unhandled_poly_type`, +`Invalid_underscore_type_in_external`. + +--- + +## Warnings + +Warnings are declared in [`compiler/ext/warnings.ml`](../compiler/ext/warnings.ml). +The default warning set is `+a-4-9-20-41-50-102` +([`compiler/ext/bsc_warnings.ml`](../compiler/ext/bsc_warnings.ml)), so +warnings 4, 9, 20, 41, 50, and 102 are disabled by default; the rest are +enabled. Fixtures use `-w +A` (everything on) so default-disabled +warnings still fire. + +Fixtures follow the naming convention `warning__.res` +so coverage gaps stay greppable. + +### Confirmed dead (no `prerr_warning` site in the compiler) + +These warning constructors exist in `warnings.ml` but are never raised +anywhere in `compiler/`. They are candidates for removal. + +| Number | Variant | Reason | +|---|---|---| +| 1 | `Comment_start` | Lexer warning; modern parser doesn't emit. | +| 2 | `Comment_not_end` | Lexer warning; modern parser doesn't emit. | +| 7 | `Method_override` | OCaml class system, not exposed by ReScript. | +| 13 | `Instance_variable_override` | OCaml class system. | +| 14 | `Illegal_backslash` | Lexer-level escape warning; parser doesn't emit. | +| 15 | `Implicit_public_methods` | OCaml class system. | +| 29 | `Eol_in_string` | Lexer-level string warning. | +| 48 | `Eliminated_optional_arguments` | Declared but never raised. | +| 50 | `Bad_docstring` | Declared but never raised; also default-disabled. | +| 105 | `Bs_fragile_external` | Declared but never raised. | +| 106 | `Bs_unimplemented_primitive` | Declared but never raised. | + +### Live but no fixture yet + +These warnings have `prerr_warning` raise sites in `compiler/` and are +reachable from regular ReScript code, but no `super_errors` fixture +currently exercises them. + +| Number | Variant | Trigger | +|---|---|---| +| 5 | `Partial_application` | `typecore.ml:2049`, `:3980` — function call in statement position returning another function. | +| 10 | `Statement_type` | `typecore.ml:2052` — expression in statement position with non-unit non-arrow type. | +| 16 | `Unerasable_optional_argument` | `typecore.ml:3525` — optional argument at the trailing position. | +| 108 | `Bs_uninterpreted_delimiters` | `compiler/common/bs_warnings.ml:29` — string literal with an unrecognized delimiter. | From 03789830aa23928a74d81fe278e96ab7b4429f4e Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:23:52 +0200 Subject: [PATCH 02/17] =?UTF-8?q?super=5Ferrors:=20add=20fixtures=20for=20?= =?UTF-8?q?typecore=20=E2=98=90=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover Unknown_literal (0z), Empty_record_literal (let bad = {}), No_value_clauses (switch with only exception clauses), Type_params_not_supported (variant spread pattern with type params), and Not_a_variant_type (variant spread pattern of non-variant). --- .../expected/empty_record_literal.res.expected | 8 ++++++++ .../expected/no_value_clauses.res.expected | 11 +++++++++++ .../expected/unknown_literal.res.expected | 8 ++++++++ ...variant_spread_pattern_not_a_variant.res.expected | 12 ++++++++++++ .../variant_spread_pattern_type_params.res.expected | 12 ++++++++++++ .../super_errors/fixtures/empty_record_literal.res | 1 + .../super_errors/fixtures/no_value_clauses.res | 4 ++++ .../super_errors/fixtures/unknown_literal.res | 1 + .../variant_spread_pattern_not_a_variant.res | 8 ++++++++ .../fixtures/variant_spread_pattern_type_params.res | 8 ++++++++ 10 files changed, 73 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/empty_record_literal.res.expected create mode 100644 tests/build_tests/super_errors/expected/no_value_clauses.res.expected create mode 100644 tests/build_tests/super_errors/expected/unknown_literal.res.expected create mode 100644 tests/build_tests/super_errors/expected/variant_spread_pattern_not_a_variant.res.expected create mode 100644 tests/build_tests/super_errors/expected/variant_spread_pattern_type_params.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/empty_record_literal.res create mode 100644 tests/build_tests/super_errors/fixtures/no_value_clauses.res create mode 100644 tests/build_tests/super_errors/fixtures/unknown_literal.res create mode 100644 tests/build_tests/super_errors/fixtures/variant_spread_pattern_not_a_variant.res create mode 100644 tests/build_tests/super_errors/fixtures/variant_spread_pattern_type_params.res diff --git a/tests/build_tests/super_errors/expected/empty_record_literal.res.expected b/tests/build_tests/super_errors/expected/empty_record_literal.res.expected new file mode 100644 index 0000000000..eea4c3efcd --- /dev/null +++ b/tests/build_tests/super_errors/expected/empty_record_literal.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/empty_record_literal.res:1:11-12 + + 1 │ let bad = {} + 2 │ + + Empty record literal {} should be type annotated or used in a record context. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/no_value_clauses.res.expected b/tests/build_tests/super_errors/expected/no_value_clauses.res.expected new file mode 100644 index 0000000000..4be6acd3c4 --- /dev/null +++ b/tests/build_tests/super_errors/expected/no_value_clauses.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/no_value_clauses.res:2:3-4:3 + + 1 │ let f = x => + 2 │ switch x { + 3 │  | exception Not_found => 1 + 4 │  } + 5 │ + + None of the patterns in this 'match' expression match values. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/unknown_literal.res.expected b/tests/build_tests/super_errors/expected/unknown_literal.res.expected new file mode 100644 index 0000000000..c002136467 --- /dev/null +++ b/tests/build_tests/super_errors/expected/unknown_literal.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/unknown_literal.res:1:9-10 + + 1 │ let x = 0z + 2 │ + + Unknown modifier 'z' for literal 0z \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/variant_spread_pattern_not_a_variant.res.expected b/tests/build_tests/super_errors/expected/variant_spread_pattern_not_a_variant.res.expected new file mode 100644 index 0000000000..36b638c039 --- /dev/null +++ b/tests/build_tests/super_errors/expected/variant_spread_pattern_not_a_variant.res.expected @@ -0,0 +1,12 @@ + + We've found a bug for you! + /.../fixtures/variant_spread_pattern_not_a_variant.res:6:8 + + 4 │ let f = (b: b) => + 5 │ switch b { + 6 │ | ...a as v => v + 7 │ | Three => One + 8 │ } + + The type a +is not a variant type \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/variant_spread_pattern_type_params.res.expected b/tests/build_tests/super_errors/expected/variant_spread_pattern_type_params.res.expected new file mode 100644 index 0000000000..eb89478018 --- /dev/null +++ b/tests/build_tests/super_errors/expected/variant_spread_pattern_type_params.res.expected @@ -0,0 +1,12 @@ + + We've found a bug for you! + /.../fixtures/variant_spread_pattern_type_params.res:6:8 + + 4 │ let f = (b: b) => + 5 │ switch b { + 6 │ | ...a as x => x + 7 │ | Three => One + 8 │ } + + The type a +has type parameters, but type parameters is not supported here. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/empty_record_literal.res b/tests/build_tests/super_errors/fixtures/empty_record_literal.res new file mode 100644 index 0000000000..149d1a86b9 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/empty_record_literal.res @@ -0,0 +1 @@ +let bad = {} diff --git a/tests/build_tests/super_errors/fixtures/no_value_clauses.res b/tests/build_tests/super_errors/fixtures/no_value_clauses.res new file mode 100644 index 0000000000..1f4afba1e0 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/no_value_clauses.res @@ -0,0 +1,4 @@ +let f = x => + switch x { + | exception Not_found => 1 + } diff --git a/tests/build_tests/super_errors/fixtures/unknown_literal.res b/tests/build_tests/super_errors/fixtures/unknown_literal.res new file mode 100644 index 0000000000..0705906df1 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/unknown_literal.res @@ -0,0 +1 @@ +let x = 0z diff --git a/tests/build_tests/super_errors/fixtures/variant_spread_pattern_not_a_variant.res b/tests/build_tests/super_errors/fixtures/variant_spread_pattern_not_a_variant.res new file mode 100644 index 0000000000..2a2aae331f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/variant_spread_pattern_not_a_variant.res @@ -0,0 +1,8 @@ +type a = {x: int} +type b = One | Two | Three + +let f = (b: b) => + switch b { + | ...a as v => v + | Three => One + } diff --git a/tests/build_tests/super_errors/fixtures/variant_spread_pattern_type_params.res b/tests/build_tests/super_errors/fixtures/variant_spread_pattern_type_params.res new file mode 100644 index 0000000000..9ff3872894 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/variant_spread_pattern_type_params.res @@ -0,0 +1,8 @@ +type a<'x> = One | Two('x) +type b = One | Two(int) | Three + +let f = (b: b) => + switch b { + | ...a as x => x + | Three => One + } From 4d24326df777bcd292681f40d2276042fbc2d650 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:24:02 +0200 Subject: [PATCH 03/17] super_errors: add fixtures for typedecl rebind variants Cover Rebind_mismatch (rebinding constructor into different extensible type) and Rebind_private (rebinding a private extension constructor as public). --- .../expected/extension_rebind_mismatch.res.expected | 10 ++++++++++ .../expected/extension_rebind_private.res.expected | 10 ++++++++++ .../fixtures/extension_rebind_mismatch.res | 5 +++++ .../super_errors/fixtures/extension_rebind_private.res | 5 +++++ 4 files changed, 30 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/extension_rebind_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/extension_rebind_private.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/extension_rebind_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/extension_rebind_private.res diff --git a/tests/build_tests/super_errors/expected/extension_rebind_mismatch.res.expected b/tests/build_tests/super_errors/expected/extension_rebind_mismatch.res.expected new file mode 100644 index 0000000000..9a8be7df8c --- /dev/null +++ b/tests/build_tests/super_errors/expected/extension_rebind_mismatch.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/extension_rebind_mismatch.res:5:15 + + 3 │ + 4 │ type a += A(int) + 5 │ type b += B = A + 6 │ + + The constructor A has type a but was expected to be of type b \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/extension_rebind_private.res.expected b/tests/build_tests/super_errors/expected/extension_rebind_private.res.expected new file mode 100644 index 0000000000..e3d9fd1288 --- /dev/null +++ b/tests/build_tests/super_errors/expected/extension_rebind_private.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/extension_rebind_private.res:5:15 + + 3 │ type t += private A(int) + 4 │ + 5 │ type t += B = A + 6 │ + + The constructor A is private \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/extension_rebind_mismatch.res b/tests/build_tests/super_errors/fixtures/extension_rebind_mismatch.res new file mode 100644 index 0000000000..da18506222 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/extension_rebind_mismatch.res @@ -0,0 +1,5 @@ +type a = .. +type b = .. + +type a += A(int) +type b += B = A diff --git a/tests/build_tests/super_errors/fixtures/extension_rebind_private.res b/tests/build_tests/super_errors/fixtures/extension_rebind_private.res new file mode 100644 index 0000000000..5c56ba18ee --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/extension_rebind_private.res @@ -0,0 +1,5 @@ +type t = .. + +type t += private A(int) + +type t += B = A From 97cd98cb892873211ee854998f2ba79fc0f000d3 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:24:13 +0200 Subject: [PATCH 04/17] =?UTF-8?q?super=5Ferrors:=20add=20fixtures=20for=20?= =?UTF-8?q?bs=5Fsyntaxerr=20=E2=98=90=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover Expect_int_or_string_or_json_literal (@as(bool) on wildcard external arg), Unsupported_predicates (@get({weird: true}) on object field), Bs_this_simple_pattern (@this with destructured self pattern), and Optional_in_uncurried_bs_attribute (@this function with optional argument). --- ...s_expect_int_or_string_or_json_literal.res.expected | 8 ++++++++ .../bs_optional_in_uncurried_bs_attribute.res.expected | 8 ++++++++ .../expected/bs_this_simple_pattern.res.expected | 8 ++++++++ .../expected/bs_unsupported_predicates.res.expected | 10 ++++++++++ .../bs_expect_int_or_string_or_json_literal.res | 1 + .../fixtures/bs_optional_in_uncurried_bs_attribute.res | 1 + .../super_errors/fixtures/bs_this_simple_pattern.res | 1 + .../fixtures/bs_unsupported_predicates.res | 3 +++ 8 files changed, 40 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/bs_expect_int_or_string_or_json_literal.res.expected create mode 100644 tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected create mode 100644 tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected create mode 100644 tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/bs_expect_int_or_string_or_json_literal.res create mode 100644 tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res create mode 100644 tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res create mode 100644 tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res diff --git a/tests/build_tests/super_errors/expected/bs_expect_int_or_string_or_json_literal.res.expected b/tests/build_tests/super_errors/expected/bs_expect_int_or_string_or_json_literal.res.expected new file mode 100644 index 0000000000..8629a05f44 --- /dev/null +++ b/tests/build_tests/super_errors/expected/bs_expect_int_or_string_or_json_literal.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/bs_expect_int_or_string_or_json_literal.res:1:21-23 + + 1 │ @val external foo: (@as(true) _, int) => unit = "foo" + 2 │ + + expect int, string literal or json literal {json|text here|json} \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected b/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected new file mode 100644 index 0000000000..e47a79cd35 --- /dev/null +++ b/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/bs_optional_in_uncurried_bs_attribute.res:1:16-56 + + 1 │ let f = @this ((self, ~x=?) => self + x->Option.getOr(0)) + 2 │ + + Uncurried function doesn't support optional arguments yet \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected b/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected new file mode 100644 index 0000000000..5a4d920a28 --- /dev/null +++ b/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/bs_this_simple_pattern.res:1:17-22 + + 1 │ let f = @this (((a, b), x) => a + b + x) + 2 │ + + %@this expect its pattern variable to be simple form \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected b/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected new file mode 100644 index 0000000000..a9b5e47af7 --- /dev/null +++ b/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/bs_unsupported_predicates.res:2:9-13 + + 1 │ type t = {.. + 2 │ @get({weird: true}) "x": int + 3 │ } + 4 │ + + unsupported predicates \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/bs_expect_int_or_string_or_json_literal.res b/tests/build_tests/super_errors/fixtures/bs_expect_int_or_string_or_json_literal.res new file mode 100644 index 0000000000..5b43399d9b --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/bs_expect_int_or_string_or_json_literal.res @@ -0,0 +1 @@ +@val external foo: (@as(true) _, int) => unit = "foo" diff --git a/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res b/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res new file mode 100644 index 0000000000..05f3f19e8d --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res @@ -0,0 +1 @@ +let f = @this ((self, ~x=?) => self + x->Option.getOr(0)) diff --git a/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res b/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res new file mode 100644 index 0000000000..0a28b8e3ec --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res @@ -0,0 +1 @@ +let f = @this (((a, b), x) => a + b + x) diff --git a/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res b/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res new file mode 100644 index 0000000000..cc622ab135 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res @@ -0,0 +1,3 @@ +type t = {.. + @get({weird: true}) "x": int +} From b765dc0579b53a1b7f94489943d10bcffcae9e80 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:26:27 +0200 Subject: [PATCH 05/17] ERROR_VARIANTS: sync with new fixtures and document dead warnings Mark the typecore/typedecl/bs_syntaxerr variants now covered by the new super_errors fixtures. Move warnings 10, 16 and 108 to the "Confirmed dead" section: Statement_type is unreachable because the only caller passes statement=false; Unerasable_optional_argument is explicitly disabled inside type_function before its check; and Bs_uninterpreted_delimiters needs a string literal with the legacy "js" delimiter, which the modern scanner never produces. Warning 5 stays "live but no fixture" with a note that the raise sites look effectively unreachable from plain ReScript source. --- tests/ERROR_VARIANTS.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 213cb0d617..72c2c88b05 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -86,7 +86,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Too_many_arguments` | ✓ | `too_many_arguments.res`, `moreArguments*.res` | | | `Abstract_wrong_label` | ? | — | typecore.ml:3502. Fires when a function literal's label doesn't match the expected arrow type. One attempted reproduction landed on `Expr_type_clash` but I didn't retest with care; trigger site is live. | | `Scoping_let_module` | ✓ | `scoping_let_module.res` | | -| `Not_a_variant_type` | ☐ | — | typecore.ml:563/613/641. Triggers in polyvariant `Ppat_type` pattern (`...t` spread in pattern); needs `[%pat_type t]` or similar pattern AST. | +| `Not_a_variant_type` | ✓ | `variant_spread_pattern_not_a_variant.res` | Pattern-level variant spread of a non-variant type. | | `Incoherent_label_order` | ? | — | typecore.ml:3894. Triggers when labeled args reorder against an arrow type that contains the label but not at the current position. Couldn't construct a reproduction that didn't hit `Apply_wrong_label` first. | | `Less_general` | ✓ | `less_general_universal.res` | | | `Modules_not_allowed` | ✓ | `super_errors_multi/Modules_not_allowed_toplevel` | Toplevel `let module(M) = …` pattern with `allow_modules=false`. | @@ -98,7 +98,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Invalid_interval` | ⚠ | — | typecore.ml:1349. Triggered by `Ppat_interval` pattern. **Verified: `Ppat_interval` has no construction site in `compiler/syntax/src/res_core.ml`** — only printer and ast_debugger handle it. | | `Invalid_for_loop_index` | ✓ | `invalid_for_loop_index.res` | | | `Invalid_for_of_pattern` | ⚠ | — | typecore.ml:3120/3152. Verified: parser `normalize_for_of_pattern` (`res_core.ml:3841`) replaces non-var / non-`_` patterns with `Ppat_any` before the typer sees them. | -| `No_value_clauses` | ☐ | — | typecore.ml:2575. Triggers when a `switch` has only exception clauses; needs `try` / `catch` with no value branch. | +| `No_value_clauses` | ✓ | `no_value_clauses.res` | | | `Exception_pattern_below_toplevel` | ✓ | `exception_pattern_below_toplevel.res` | | | `Inlined_record_escape` | ✓ | `inline_record_escape.res` | | | `Inlined_record_expected` | ✓ | `inlined_record_expected.res`, `super_errors_multi/Cross_inline_record_constructor` | | @@ -107,12 +107,12 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Break_outside_loop` | ✓ | `break_outside_loop.res`, `break_in_nested_function.res` | | | `Continue_outside_loop` | ✓ | `continue_outside_loop.res`, `continue_in_nested_function.res` | | | `Literal_overflow` | ✓ | `intoverflow.res` | | -| `Unknown_literal` | ☐ | — | typecore.ml:279/283. **Confirmed reachable**: `let x = 0z` produces `Unknown modifier 'z' for literal 0z`. The lexer (`res_scanner.ml:302`) accepts any `g..z` / `G..Z` suffix; unknown ones fire here. | +| `Unknown_literal` | ✓ | `unknown_literal.res` | | | `Illegal_letrec_pat` | ✓ | `illegal_letrec_pat.res` | | -| `Empty_record_literal` | ☐ | — | typecore.ml:2747. **Confirmed reachable**: `let bad = {}` (no type annotation) produces `Empty record literal {} should be type annotated or used in a record context.`. With an annotation, `Labels_missing` fires first. | +| `Empty_record_literal` | ✓ | `empty_record_literal.res` | | | `Uncurried_arity_mismatch` | ✓ | `arity_mismatch3.res` etc. | | | `Field_not_optional` | ✓ | `fieldNotOptional.res` | | -| `Type_params_not_supported` | ☐ | — | typecore.ml:635. Variant spread pattern (`| ...a`) where `a` has type params. Existing `variant_spread_type_parameters.res` covers the typedecl path; this is a separate pattern-level path. | +| `Type_params_not_supported` | ✓ | `variant_spread_pattern_type_params.res` | Pattern-level variant spread (`| ...a as v`) where `a` has type params; typedecl path covered by `variant_spread_type_parameters.res`. | | `Field_access_on_dict_type` | ✓ | `field_access_on_dict_type.res` | | | `Jsx_not_enabled` | ☐ | — | typecore.ml:218/3470. Fires when JSX expressions are used without `-bs-jsx N`. Reachable but the existing `super_errors` runner always passes `-bs-jsx 4`. | @@ -140,9 +140,9 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Cannot_extend_private_type` | ✓ | `cannot_extend_private_type.res` | | | `Not_extensible_type` | ✓ | `not_extensible_type.res` | | | `Extension_mismatch` | ☐ | — | Cross-module extension declaration mismatch via `.resi`/`.res`. | -| `Rebind_wrong_type` | ☐ | — | typedecl.ml:1653. `exception X(int) = OtherWithStringArg`. ReScript **does** support exception rebinding (parser at `res_core.ml:6660` emits `Pext_rebind`). My prior claim that the syntax wasn't exposed was wrong. | -| `Rebind_mismatch` | ☐ | — | typedecl.ml:1681. Rebinding from a different extensible type. Same parser support as above. | -| `Rebind_private` | ☐ | — | typedecl.ml:1684. Rebinding a private constructor as public. Same parser support. | +| `Rebind_wrong_type` | ? | — | typedecl.ml:1653. Fires when source constructor's result type doesn't unify with target's. For exceptions both are `exn`; for extension types both share the extensible parent. I couldn't construct a triggering shape — the rebind succeeds for shapes the parser will accept. | +| `Rebind_mismatch` | ✓ | `extension_rebind_mismatch.res` | Rebinding constructor into a different extensible type. | +| `Rebind_private` | ✓ | `extension_rebind_private.res` | Rebinding a private extension constructor as public. | | `Bad_variance` | ✓ | `bad_variance.res`, `bad_variance_contra.res` | | | `Unavailable_type_constructor` | ☐ | — | typedecl.ml:778. Requires a type path findable at parse time but missing during constraint enforcement; only cross-unit scenarios. | | `Bad_fixed_type` | ? | — | typedecl.ml:190/193. `set_fixed_row` runs when `is_fixed_type` returns true — requires an open object `{..f: t}` or open polyvariant `[> #A]` as `ptype_manifest`. Then if the expanded head isn't `Tvariant` / `Tobject` (line 190) or the row variable isn't `Tvar` (line 193), error. Reachable in principle via an alias chain that collapses the open row, but I haven't constructed one. | @@ -278,13 +278,13 @@ FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../c | Variant | Status | Fixture | Notes | |---|---|---|---| -| `Unsupported_predicates` | ☐ | — | ast_attributes.ml:55, 69. **Reachable** via object type field with unknown `@get` / `@set` predicate keys (e.g. `{@get({weird: true}) "x": int}` on object type field). Called from `process_method_attributes_rev` → `process_getter_setter` for `Ptyp_object` fields. | -| `Conflict_bs_bs_this_bs_meth` | ☐ | — | bs_syntaxerr.ml:68. `@this` and `@meth` co-applied. `@this` **is** accepted (`let f = @this (self => self)` parses and goes through `to_method_callback`); needs constructing a case where the conflict check fires. | +| `Unsupported_predicates` | ✓ | `bs_unsupported_predicates.res` | `@get({weird: true})` on object type field. | +| `Conflict_bs_bs_this_bs_meth` | ? | — | bs_syntaxerr.ml:68. `@this` and `@meth` co-applied. `@this` **is** accepted (`let f = @this (self => self)` parses and goes through `to_method_callback`); I haven't constructed a case where the conflict check actually fires. | | `Duplicated_bs_deriving` | ✓ | `duplicated_bs_deriving.res` | | | `Conflict_attributes` | ✓ | `bs_conflict_attributes.res` | | | `Expect_int_literal` | ✓ | `bs_expect_int_literal.res` | | | `Expect_string_literal` | ✓ | `bs_expect_string_literal.res` | | -| `Expect_int_or_string_or_json_literal` | ☐ | — | ast_attributes.ml:268. **Reachable** via `_ [@as ]` in an external where the payload is neither int nor a delimited string (e.g. `@as(true)` or `@as(())`). Called from `refine_arg_type` in `ast_external_process.ml:78` for wildcard external arguments. | +| `Expect_int_or_string_or_json_literal` | ✓ | `bs_expect_int_or_string_or_json_literal.res` | `@as(true)` on a wildcard external argument. | | `Unhandled_poly_type` | ? | — | ast_core_type.ml:141. Triggers in `list_of_arrow` when an arrow chain contains a `Ptyp_poly`. The parser doesn't normally produce inline poly types inside arrows, but record fields can have polytypes that flow through these utilities. | | `Invalid_underscore_type_in_external` | ? | — | ast_external_process.ml:107/132. Needs `_` in optional-label external position with no `@as`. Probably reachable in `@@obj` externals; not yet verified. | | `Invalid_bs_string_type` | ✓ | `bs_invalid_bs_string_type.res` | | @@ -295,8 +295,8 @@ FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../c | `Not_supported_directive_in_bs_return` | ✓ | `bs_not_supported_directive_in_bs_return.res` | | | `Expect_opt_in_bs_return_to_opt` | ✓ | `bs_expect_opt_in_bs_return_to_opt.res` | | | `Misplaced_label_syntax` | ⚠ | — | bs_syntaxerr.ml:116. Only fires from `check_and_discard` in `ast_exp_apply.ml:49`, applied to the args of `->`, `#=`, `##` operators. The parser always emits those args as `Nolabel`. | -| `Optional_in_uncurried_bs_attribute` | ☐ | — | bs_syntaxerr.ml:112. Called from `ast_uncurry_gen.ml:34/40` for `@this` body args. `@this` **is** accepted on regular function literals (`let f = @this (self => self)` works); needs an `@this` function with an optional arg to trigger. | -| `Bs_this_simple_pattern` | ☐ | — | ast_uncurry_gen.ml:32. Same `@this` family — fires when the self pattern isn't a single variable. Reachable via `@this`-annotated function literal with a destructured self pattern. | +| `Optional_in_uncurried_bs_attribute` | ✓ | `bs_optional_in_uncurried_bs_attribute.res` | `@this` function with optional argument. | +| `Bs_this_simple_pattern` | ✓ | `bs_this_simple_pattern.res` | `@this` with destructured self pattern. | | `Experimental_feature_not_enabled` | ✓ | `let_unwrap_on_top_level_not_enabled.res` (and other let-unwrap variants) | Currently only `LetUnwrap` is checked. | | `LetUnwrap_not_supported_in_position` | ✓ | `let_unwrap_on_top_level.res`, `let_unwrap_on_not_supported_variant.res` | | @@ -515,6 +515,9 @@ anywhere in `compiler/`. They are candidates for removal. | 50 | `Bad_docstring` | Declared but never raised; also default-disabled. | | 105 | `Bs_fragile_external` | Declared but never raised. | | 106 | `Bs_unimplemented_primitive` | Declared but never raised. | +| 10 | `Statement_type` | Raised at typecore.ml:2052 inside `check_application_result`, but `statement` is always `false` at the only call site (typecore.ml:3983); sequence statements hit `Expr_type_clash` via `type_statement` unifying to `unit`. | +| 16 | `Unerasable_optional_argument` | Raised at typecore.ml:3526, but `type_function` (typecore.ml:3479) explicitly disables this warning before the check runs (`Warnings.parse_options false "-16"`). | +| 108 | `Bs_uninterpreted_delimiters` | Raised at bs_warnings.ml:29 for `Pconst_string` with delimiter `"js"`; the modern scanner has no `{js\|...\|js}` form and template strings don't tag with `"js"`. | ### Live but no fixture yet @@ -524,7 +527,4 @@ currently exercises them. | Number | Variant | Trigger | |---|---|---| -| 5 | `Partial_application` | `typecore.ml:2049`, `:3980` — function call in statement position returning another function. | -| 10 | `Statement_type` | `typecore.ml:2052` — expression in statement position with non-unit non-arrow type. | -| 16 | `Unerasable_optional_argument` | `typecore.ml:3525` — optional argument at the trailing position. | -| 108 | `Bs_uninterpreted_delimiters` | `compiler/common/bs_warnings.ml:29` — string literal with an unrecognized delimiter. | +| 5 | `Partial_application` | `typecore.ml:2049`, `:3980` — fires from `check_application_result` and a guarded branch in the `ignore` special case. The 3980 branch needs `not total_app`, which would require `ignore(arg, ...)` partial application — syntactically non-sensical. The 2049 site fires via a delayed check whose only path is hard to trigger from plain source. Status: live raise sites but I couldn't construct a reproduction; may be effectively dead. | From a39366c25115c60d52df9f24cad1af4956095acb Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:33:55 +0200 Subject: [PATCH 06/17] super_errors_multi: add fixtures for includecore type_mismatch variants Cover Kind (record vs variant), Constraint (impl adds constraint that the abstract interface doesn't), Manifest (different manifest types), Variance (interface annotates +'a, impl differs), Field_arity (constructor arg counts differ), Field_names (record field names differ at position), Record_representation (boxed vs @unboxed), and Immediate (@immediate vs non-immediate manifest). --- .../Iface_constraint_mismatch.expected | 20 +++++++++++++++++++ .../Iface_field_arity_mismatch.expected | 19 ++++++++++++++++++ .../Iface_field_names_mismatch.expected | 19 ++++++++++++++++++ .../Iface_immediate_mismatch.expected | 19 ++++++++++++++++++ .../expected/Iface_kind_mismatch.expected | 19 ++++++++++++++++++ .../expected/Iface_manifest_mismatch.expected | 18 +++++++++++++++++ ...ce_record_representation_mismatch.expected | 20 +++++++++++++++++++ .../expected/Iface_variance_mismatch.expected | 19 ++++++++++++++++++ .../Iface_constraint_mismatch/Foo.res | 1 + .../Iface_constraint_mismatch/Foo.resi | 1 + .../Iface_field_arity_mismatch/Foo.res | 1 + .../Iface_field_arity_mismatch/Foo.resi | 1 + .../Iface_field_names_mismatch/Foo.res | 1 + .../Iface_field_names_mismatch/Foo.resi | 1 + .../fixtures/Iface_immediate_mismatch/Foo.res | 1 + .../Iface_immediate_mismatch/Foo.resi | 2 ++ .../fixtures/Iface_kind_mismatch/Foo.res | 1 + .../fixtures/Iface_kind_mismatch/Foo.resi | 1 + .../fixtures/Iface_manifest_mismatch/Foo.res | 1 + .../fixtures/Iface_manifest_mismatch/Foo.resi | 1 + .../Foo.res | 1 + .../Foo.resi | 2 ++ .../fixtures/Iface_variance_mismatch/Foo.res | 1 + .../fixtures/Iface_variance_mismatch/Foo.resi | 1 + 24 files changed, 171 insertions(+) create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_constraint_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_field_arity_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_field_names_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_immediate_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_kind_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_manifest_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_record_representation_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_variance_mismatch.expected create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.resi create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.resi diff --git a/tests/build_tests/super_errors_multi/expected/Iface_constraint_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_constraint_mismatch.expected new file mode 100644 index 0000000000..f829519e4e --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_constraint_mismatch.expected @@ -0,0 +1,20 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_constraint_mismatch/Foo.res:1:1-35 + + 1 │ type t<'a> = 'a constraint 'a = int + 2 │ + + The implementation /.../fixtures/Iface_constraint_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_constraint_mismatch/foo.cmi: + Type declarations do not match: + type t<'a> = 'a + constraint 'a = int + is not included in + type t<'a> + /.../fixtures/Iface_constraint_mismatch/Foo.resi:1:1-10: + Expected declaration + /.../fixtures/Iface_constraint_mismatch/Foo.res:1:1-35: + Actual declaration + Their constraints differ. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_field_arity_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_field_arity_mismatch.expected new file mode 100644 index 0000000000..5f1609f1f9 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_field_arity_mismatch.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_field_arity_mismatch/Foo.res:1:1-19 + + 1 │ type t = A(int) | B + 2 │ + + The implementation /.../fixtures/Iface_field_arity_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_field_arity_mismatch/foo.cmi: + Type declarations do not match: + type t = A(int) | B + is not included in + type t = A(int, int) | B + /.../fixtures/Iface_field_arity_mismatch/Foo.resi:1:1-24: + Expected declaration + /.../fixtures/Iface_field_arity_mismatch/Foo.res:1:1-19: + Actual declaration + The arities for field A differ. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_field_names_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_field_names_mismatch.expected new file mode 100644 index 0000000000..50e347af7e --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_field_names_mismatch.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_field_names_mismatch/Foo.res:1:1-25 + + 1 │ type t = {a: int, b: int} + 2 │ + + The implementation /.../fixtures/Iface_field_names_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_field_names_mismatch/foo.cmi: + Type declarations do not match: + type t = {a: int, b: int} + is not included in + type t = {x: int, y: int} + /.../fixtures/Iface_field_names_mismatch/Foo.resi:1:1-25: + Expected declaration + /.../fixtures/Iface_field_names_mismatch/Foo.res:1:1-25: + Actual declaration + Fields number 1 have different names, a and x. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_immediate_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_immediate_mismatch.expected new file mode 100644 index 0000000000..7b6273d7c0 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_immediate_mismatch.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_immediate_mismatch/Foo.res:1:1-15 + + 1 │ type t = string + 2 │ + + The implementation /.../fixtures/Iface_immediate_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_immediate_mismatch/foo.cmi: + Type declarations do not match: + type t = string + is not included in + @immediate type t + /.../fixtures/Iface_immediate_mismatch/Foo.resi:2:1-6: + Expected declaration + /.../fixtures/Iface_immediate_mismatch/Foo.res:1:1-15: + Actual declaration + the first is not an immediate type. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_kind_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_kind_mismatch.expected new file mode 100644 index 0000000000..c00ea7282a --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_kind_mismatch.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_kind_mismatch/Foo.res:1:1-17 + + 1 │ type t = {x: int} + 2 │ + + The implementation /.../fixtures/Iface_kind_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_kind_mismatch/foo.cmi: + Type declarations do not match: + type t = {x: int} + is not included in + type t = A | B + /.../fixtures/Iface_kind_mismatch/Foo.resi:1:1-14: + Expected declaration + /.../fixtures/Iface_kind_mismatch/Foo.res:1:1-17: + Actual declaration + Their kinds differ. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_manifest_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_manifest_mismatch.expected new file mode 100644 index 0000000000..7271fca599 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_manifest_mismatch.expected @@ -0,0 +1,18 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_manifest_mismatch/Foo.res:1:1-15 + + 1 │ type t = string + 2 │ + + The implementation /.../fixtures/Iface_manifest_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_manifest_mismatch/foo.cmi: + Type declarations do not match: + type t = string + is not included in + type t = int + /.../fixtures/Iface_manifest_mismatch/Foo.resi:1:1-12: + Expected declaration + /.../fixtures/Iface_manifest_mismatch/Foo.res:1:1-15: + Actual declaration \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_record_representation_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_record_representation_mismatch.expected new file mode 100644 index 0000000000..7eb863109e --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_record_representation_mismatch.expected @@ -0,0 +1,20 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_record_representation_mismatch/Foo.res:1:1-17 + + 1 │ type t = {x: int} + 2 │ + + The implementation /.../fixtures/Iface_record_representation_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_record_representation_mismatch/foo.cmi: + Type declarations do not match: + type t = {x: int} + is not included in + @unboxed type t = {x: int} + /.../fixtures/Iface_record_representation_mismatch/Foo.resi:2:1-17: + Expected declaration + /.../fixtures/Iface_record_representation_mismatch/Foo.res:1:1-17: + Actual declaration + Their internal representations differ: + the second declaration uses unboxed representation. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/expected/Iface_variance_mismatch.expected b/tests/build_tests/super_errors_multi/expected/Iface_variance_mismatch.expected new file mode 100644 index 0000000000..39b96b60e2 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_variance_mismatch.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_variance_mismatch/Foo.res:1:1-21 + + 1 │ type t<'a> = 'a => 'a + 2 │ + + The implementation /.../fixtures/Iface_variance_mismatch/Foo.res + does not match the interface /.../fixtures/Iface_variance_mismatch/foo.cmi: + Type declarations do not match: + type t<'a> = 'a => 'a + is not included in + type t<+'a> + /.../fixtures/Iface_variance_mismatch/Foo.resi:1:1-11: + Expected declaration + /.../fixtures/Iface_variance_mismatch/Foo.res:1:1-21: + Actual declaration + Their variances do not agree. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.res new file mode 100644 index 0000000000..5592f33e50 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.res @@ -0,0 +1 @@ +type t<'a> = 'a constraint 'a = int diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.resi new file mode 100644 index 0000000000..cfacdaa67e --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_constraint_mismatch/Foo.resi @@ -0,0 +1 @@ +type t<'a> diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.res new file mode 100644 index 0000000000..f097aee38d --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.res @@ -0,0 +1 @@ +type t = A(int) | B diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.resi new file mode 100644 index 0000000000..ce215b5419 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_field_arity_mismatch/Foo.resi @@ -0,0 +1 @@ +type t = A(int, int) | B diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.res new file mode 100644 index 0000000000..e2e17af745 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.res @@ -0,0 +1 @@ +type t = {a: int, b: int} diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.resi new file mode 100644 index 0000000000..b5eab82245 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_field_names_mismatch/Foo.resi @@ -0,0 +1 @@ +type t = {x: int, y: int} diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.res new file mode 100644 index 0000000000..c6a8e288e4 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.res @@ -0,0 +1 @@ +type t = string diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.resi new file mode 100644 index 0000000000..fa534f94c0 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_immediate_mismatch/Foo.resi @@ -0,0 +1,2 @@ +@immediate +type t diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.res new file mode 100644 index 0000000000..78f2639963 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.res @@ -0,0 +1 @@ +type t = {x: int} diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.resi new file mode 100644 index 0000000000..cdb40e3704 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_kind_mismatch/Foo.resi @@ -0,0 +1 @@ +type t = A | B diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.res new file mode 100644 index 0000000000..c6a8e288e4 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.res @@ -0,0 +1 @@ +type t = string diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.resi new file mode 100644 index 0000000000..975adb5316 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_manifest_mismatch/Foo.resi @@ -0,0 +1 @@ +type t = int diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.res new file mode 100644 index 0000000000..78f2639963 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.res @@ -0,0 +1 @@ +type t = {x: int} diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.resi new file mode 100644 index 0000000000..22a0ef7506 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_record_representation_mismatch/Foo.resi @@ -0,0 +1,2 @@ +@unboxed +type t = {x: int} diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.res new file mode 100644 index 0000000000..5f2131755d --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.res @@ -0,0 +1 @@ +type t<'a> = 'a => 'a diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.resi new file mode 100644 index 0000000000..3b5ed7d76b --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_variance_mismatch/Foo.resi @@ -0,0 +1 @@ +type t<+'a> From baa6e08ac6e4c023f9d3f11c6fc3900310b598bd Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:34:07 +0200 Subject: [PATCH 07/17] super_errors: add typetexp Type_mismatch and Not_a_variant fixtures Type_mismatch fires when applying a type constructor with arguments that violate a constraint clause (`type t<'a> = 'a constraint 'a = int` called with `t`). Not_a_variant fires when a polyvariant inherits from a non-polyvariant (`[#X | int]`). Also catalog the new fixtures, mark Conflict_bs_bs_this_bs_meth as dead (declared but never raised), and downgrade Rebind_wrong_type to ? since I couldn't construct a triggering shape. --- tests/ERROR_VARIANTS.md | 22 +++++++++---------- .../typetexp_not_a_variant.res.expected | 9 ++++++++ .../typetexp_type_mismatch.res.expected | 9 ++++++++ .../fixtures/typetexp_not_a_variant.res | 2 ++ .../fixtures/typetexp_type_mismatch.res | 2 ++ 5 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/typetexp_not_a_variant.res.expected create mode 100644 tests/build_tests/super_errors/expected/typetexp_type_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/typetexp_not_a_variant.res create mode 100644 tests/build_tests/super_errors/fixtures/typetexp_type_mismatch.res diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 72c2c88b05..45b95a2277 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -199,12 +199,12 @@ Type-expression errors. Source: [typetexp.ml:28](../compiler/ml/typetexp.ml). | `Unbound_type_constructor` | ✓ | `typetexp_unbound_type_constructor.res` | | | `Unbound_type_constructor_2` | ? | — | typetexp.ml:475/619. Triggers in object / polyvariant inheritance where the inherited type's row variable is `Tvar` with a path. Hard to construct, but not provably dead. | | `Type_arity_mismatch` | ✓ | `type_arity_mismatch.res` | | -| `Type_mismatch` | ☐ | — | typetexp.ml:368/373. Type-constructor application with `unify_param` failure (368) or `enforce_constraints` failure (373); mostly subsumed by `Constraint_failed`. | +| `Type_mismatch` | ✓ | `typetexp_type_mismatch.res` | Type-constructor application that violates a `constraint 'a = …` on the declaration. | | `Alias_type_mismatch` | ✓ | `typetexp_alias_type_mismatch.res` | | | `Present_has_conjunction` | ? | — | typetexp.ml:452. Polyvariant tag with conjunction (`&`) typing path. ReScript's parser doesn't have a `&` polyvariant operator that I can find, but the AST `Rtag` constructor supports a conjunction list, so PPX-generated AST could reach it. | | `Present_has_no_type` | ? | — | typetexp.ml:501. Same `Rtag`-with-conjunction family. | | `Constructor_mismatch` | ✓ | `polyvariant_constructor_mismatch.res` | | -| `Not_a_variant` | ☐ | — | typetexp.ml:476. Polyvariant inheritance from non-variant. | +| `Not_a_variant` | ✓ | `typetexp_not_a_variant.res` | Polyvariant `[#X \| a]` where `a` is not a polyvariant. | | `Variant_tags` | ⚠ | — | typetexp.ml:39. Raised at typecore.ml:342, 349, 367 via `Tags` exception from `ctype.ml`. **Verified: `exception Tags` is defined (ctype.ml:60) but never raised in `compiler/`.** Confirmed dead. | | `Invalid_variable_name` | ✓ | `invalid_type_variable_name.res` | | | `Cannot_quantify` | ? | — | typetexp.ml:540. Triggers in `Ptyp_poly` translation when a quantified variable becomes non-generic. Every value-level reproduction lands on `Less_general` first, but type-level constructions with constraints might still reach it. | @@ -254,19 +254,19 @@ Source: [includecore.ml:159](../compiler/ml/includecore.ml). |---|---|---|---| | `Arity` | ✓ | `definition_mismatch.res` | | | `Privacy` | ✓ | `super_errors_multi/Iface_privacy_mismatch` | | -| `Kind` | ☐ | — | E.g. record vs variant mismatch between `.resi` and `.res`. | -| `Constraint` | ☐ | — | Type abbreviation constraint mismatch. | -| `Manifest` | ☐ | — | Manifest type differs. | -| `Variance` | ☐ | — | Variance annotations differ. | +| `Kind` | ✓ | `super_errors_multi/Iface_kind_mismatch` | Record-in-impl vs variant-in-interface. | +| `Constraint` | ✓ | `super_errors_multi/Iface_constraint_mismatch` | Implementation adds a `constraint 'a = …`; interface has none. | +| `Manifest` | ✓ | `super_errors_multi/Iface_manifest_mismatch` | Manifest types differ (`int` vs `string`). | +| `Variance` | ✓ | `super_errors_multi/Iface_variance_mismatch` | Interface annotates `+'a`; implementation's inferred variance differs. | | `Field_type` | ✓ | `super_errors_multi/Iface_type_decl_record` | | | `Field_mutable` | ✓ | `super_errors_multi/Iface_field_mutable_mismatch` | | | `Field_optional` | ✓ | `super_errors_multi/Iface_field_optional_mismatch` | | -| `Field_arity` | ☐ | — | Constructor with different argument count. | -| `Field_names` | ☐ | — | Record field names differ at position. | +| `Field_arity` | ✓ | `super_errors_multi/Iface_field_arity_mismatch` | Constructor with different argument count between `.resi` / `.res`. | +| `Field_names` | ✓ | `super_errors_multi/Iface_field_names_mismatch` | Record field names differ at the same position. | | `Field_missing` | ✓ | `super_errors_multi/Iface_missing_value` (indirect) | | -| `Record_representation` | ☐ | — | Boxed-vs-unboxed record representation mismatch. | +| `Record_representation` | ✓ | `super_errors_multi/Iface_record_representation_mismatch` | Interface declares `@unboxed`; implementation is boxed. | | `Unboxed_representation` | ✓ | `super_errors_multi/Iface_unboxed_variant_mismatch` | | -| `Immediate` | ☐ | — | `@immediate` attribute mismatch. | +| `Immediate` | ✓ | `super_errors_multi/Iface_immediate_mismatch` | Interface adds `@immediate`; implementation manifests a non-immediate (`string`). | | `Tag_name` | ✓ | `super_errors_multi/Iface_tag_name_mismatch` | | | `Variant_representation` | ✓ | `super_errors_multi/Iface_variant_representation_mismatch` | | @@ -279,7 +279,7 @@ FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../c | Variant | Status | Fixture | Notes | |---|---|---|---| | `Unsupported_predicates` | ✓ | `bs_unsupported_predicates.res` | `@get({weird: true})` on object type field. | -| `Conflict_bs_bs_this_bs_meth` | ? | — | bs_syntaxerr.ml:68. `@this` and `@meth` co-applied. `@this` **is** accepted (`let f = @this (self => self)` parses and goes through `to_method_callback`); I haven't constructed a case where the conflict check actually fires. | +| `Conflict_bs_bs_this_bs_meth` | ⚠ | — | bs_syntaxerr.ml:29 declares the variant but `Bs_syntaxerr.err _ Conflict_bs_bs_this_bs_meth` is **never raised** anywhere in `compiler/`. | | `Duplicated_bs_deriving` | ✓ | `duplicated_bs_deriving.res` | | | `Conflict_attributes` | ✓ | `bs_conflict_attributes.res` | | | `Expect_int_literal` | ✓ | `bs_expect_int_literal.res` | | diff --git a/tests/build_tests/super_errors/expected/typetexp_not_a_variant.res.expected b/tests/build_tests/super_errors/expected/typetexp_not_a_variant.res.expected new file mode 100644 index 0000000000..2b31397965 --- /dev/null +++ b/tests/build_tests/super_errors/expected/typetexp_not_a_variant.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/typetexp_not_a_variant.res:2:16 + + 1 │ type a = int + 2 │ type b = [#X | a] + 3 │ + + The type a does not expand to a polymorphic variant type \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/typetexp_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/typetexp_type_mismatch.res.expected new file mode 100644 index 0000000000..6643198019 --- /dev/null +++ b/tests/build_tests/super_errors/expected/typetexp_type_mismatch.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/typetexp_type_mismatch.res:2:12-17 + + 1 │ type t<'a> = 'a constraint 'a = int + 2 │ type x = t<string> + 3 │ + + This type string should be an instance of type int \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/typetexp_not_a_variant.res b/tests/build_tests/super_errors/fixtures/typetexp_not_a_variant.res new file mode 100644 index 0000000000..6ca2a4f099 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/typetexp_not_a_variant.res @@ -0,0 +1,2 @@ +type a = int +type b = [#X | a] diff --git a/tests/build_tests/super_errors/fixtures/typetexp_type_mismatch.res b/tests/build_tests/super_errors/fixtures/typetexp_type_mismatch.res new file mode 100644 index 0000000000..de39d0e28b --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/typetexp_type_mismatch.res @@ -0,0 +1,2 @@ +type t<'a> = 'a constraint 'a = int +type x = t From baa1bed92580888941a8d8396e8645a399c42b99 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:42:47 +0200 Subject: [PATCH 08/17] super_errors: add fixtures for untagged_variants + typemod Signature_expected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers every ☐ variant in ast_untagged_variants.ml: OnlyOneUnknown, AtMostOneObject/Instance/Function/Number/Bigint/Boolean, DuplicateLiteral, ConstructorMoreThanOneArg, InvalidVariantAsAnnotation, Duplicated_bs_as, InvalidVariantTagAnnotation, TagFieldNameConflict. Also adds typemod_signature_expected.res — a `with type` on an inner functor-typed module triggers Signature_expected when the parent sig-extraction hits a non-Mty_signature module type. Updates the catalog and marks ast_utf8_string_interp.* as dead (production pipeline never calls check_and_transform; only OUnit tests do). --- tests/ERROR_VARIANTS.md | 61 +++++++++++-------- .../UntaggedAtMostOneBigint.res.expected | 9 +++ .../UntaggedAtMostOneBoolean.res.expected | 9 +++ .../UntaggedAtMostOneFunction.res.expected | 9 +++ .../UntaggedAtMostOneInstance.res.expected | 9 +++ .../UntaggedAtMostOneNumber.res.expected | 9 +++ .../UntaggedAtMostOneObject.res.expected | 10 +++ ...ggedConstructorMoreThanOneArg.res.expected | 9 +++ .../UntaggedDuplicateLiteral.res.expected | 9 +++ .../UntaggedDuplicatedBsAs.res.expected | 9 +++ ...gedInvalidVariantAsAnnotation.res.expected | 8 +++ ...edInvalidVariantTagAnnotation.res.expected | 9 +++ .../UntaggedOnlyOneUnknown.res.expected | 10 +++ .../UntaggedTagFieldNameConflict.res.expected | 11 ++++ .../typemod_signature_expected.res.expected | 10 +++ .../fixtures/UntaggedAtMostOneBigint.res | 2 + .../fixtures/UntaggedAtMostOneBoolean.res | 2 + .../fixtures/UntaggedAtMostOneFunction.res | 2 + .../fixtures/UntaggedAtMostOneInstance.res | 2 + .../fixtures/UntaggedAtMostOneNumber.res | 2 + .../fixtures/UntaggedAtMostOneObject.res | 4 ++ .../UntaggedConstructorMoreThanOneArg.res | 2 + .../fixtures/UntaggedDuplicateLiteral.res | 2 + .../fixtures/UntaggedDuplicatedBsAs.res | 2 + .../UntaggedInvalidVariantAsAnnotation.res | 1 + .../UntaggedInvalidVariantTagAnnotation.res | 2 + .../fixtures/UntaggedOnlyOneUnknown.res | 4 ++ .../fixtures/UntaggedTagFieldNameConflict.res | 4 ++ .../fixtures/typemod_signature_expected.res | 4 ++ 29 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneBigint.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneBoolean.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneFunction.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneNumber.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedAtMostOneObject.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedConstructorMoreThanOneArg.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedDuplicateLiteral.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedDuplicatedBsAs.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedInvalidVariantAsAnnotation.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedOnlyOneUnknown.res.expected create mode 100644 tests/build_tests/super_errors/expected/UntaggedTagFieldNameConflict.res.expected create mode 100644 tests/build_tests/super_errors/expected/typemod_signature_expected.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBigint.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBoolean.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneFunction.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneInstance.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneNumber.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedAtMostOneObject.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedConstructorMoreThanOneArg.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedDuplicateLiteral.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedDuplicatedBsAs.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantAsAnnotation.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedOnlyOneUnknown.res create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedTagFieldNameConflict.res create mode 100644 tests/build_tests/super_errors/fixtures/typemod_signature_expected.res diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 45b95a2277..37db126679 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -310,26 +310,26 @@ Untagged-variant validation errors. Source: [ast_untagged_variants.ml:52](../com | Variant | Status | Fixture | Notes | |---|---|---|---| -| `OnlyOneUnknown` | ☐ | — | More than one constructor with `Unknown` type. | -| `AtMostOneObject` | ☐ | — | More than one object constructor. | -| `AtMostOneInstance` | ☐ | — | More than one constructor for a given JS instance. | -| `AtMostOneFunction` | ☐ | — | More than one function-typed constructor. | +| `OnlyOneUnknown` | ✓ | `UntaggedOnlyOneUnknown.res` | Abstract payload alongside another payload-carrying case. | +| `AtMostOneObject` | ✓ | `UntaggedAtMostOneObject.res` | Two record payloads in the same untagged variant. | +| `AtMostOneInstance` | ✓ | `UntaggedAtMostOneInstance.res` | Two cases with the same JS instance type (`Date.t` in both). | +| `AtMostOneFunction` | ✓ | `UntaggedAtMostOneFunction.res` | Two function-typed payloads. | | `AtMostOneString` | ✓ | `UntaggedNonUnary*.res` (some sub-cases) | | -| `AtMostOneNumber` | ☐ | — | | -| `AtMostOneBigint` | ☐ | — | | -| `AtMostOneBoolean` | ☐ | — | | -| `DuplicateLiteral` | ☐ | — | Same `@as` literal on multiple constructors. | -| `ConstructorMoreThanOneArg` | ☐ | — | Multi-arg constructor in untagged variant. | +| `AtMostOneNumber` | ✓ | `UntaggedAtMostOneNumber.res` | `int` and `float` payloads collide on the number runtime check. | +| `AtMostOneBigint` | ✓ | `UntaggedAtMostOneBigint.res` | Two bigint payloads. | +| `AtMostOneBoolean` | ✓ | `UntaggedAtMostOneBoolean.res` | Two boolean payloads. | +| `DuplicateLiteral` | ✓ | `UntaggedDuplicateLiteral.res` | `@as("x")` on two different constructors. | +| `ConstructorMoreThanOneArg` | ✓ | `UntaggedConstructorMoreThanOneArg.res` | `A(int, int)` payload in an untagged variant. | ### `error` | Variant | Status | Fixture | Notes | |---|---|---|---| -| `InvalidVariantAsAnnotation` | ☐ | — | | -| `Duplicated_bs_as` | ☐ | — | Two `@as` attributes on same constructor. | -| `InvalidVariantTagAnnotation` | ☐ | — | | -| `InvalidUntaggedVariantDefinition` | ✓ | `UntaggedUnknown.res`, `UntaggedNonUnary*.res`, `UntaggedTupleAndArray.res`, `UntaggedImplIntf.res` | | -| `TagFieldNameConflict` | ☐ | — | | +| `InvalidVariantAsAnnotation` | ✓ | `UntaggedInvalidVariantAsAnnotation.res` | `@as(foo)` with a non-`null` / non-`undefined` identifier payload. | +| `Duplicated_bs_as` | ✓ | `UntaggedDuplicatedBsAs.res` | Two `@as("...")` attributes on the same constructor. | +| `InvalidVariantTagAnnotation` | ✓ | `UntaggedInvalidVariantTagAnnotation.res` | `@tag(123)` (non-string payload). | +| `InvalidUntaggedVariantDefinition` | ✓ | `UntaggedUnknown.res`, `UntaggedNonUnary*.res`, `UntaggedTupleAndArray.res`, `UntaggedImplIntf.res`, etc. | | +| `TagFieldNameConflict` | ✓ | `UntaggedTagFieldNameConflict.res` | `@tag("kind")` plus inline record field named `kind` on a constructor. | --- @@ -396,7 +396,7 @@ PPX-runtime errors. Source: [cmd_ast_exception.ml:24](../compiler/core/cmd_ast_e | `compiler/ml/translmod.ml` | `Fragile_pattern_in_toplevel` | ✓ | `fragile_pattern_toplevel.res` | | | `compiler/ml/transl_recmodule.ml` | `Circular_dependency` | ✓ | `recmodule_circular_dependency.res` | | | `compiler/ml/rec_check.ml` | `Illegal_letrec_expr` | ✓ | `illegal_letrec_expr.res` | | -| `compiler/ml/syntaxerr.ml` | `Variable_in_scope` | ☐ | — | Raised by `Ast_helper.Typ.varify_constructors` (`ast_helper.ml:84`, `ast_helper0.ml:74`) when a type variable shadows an outer one during alias expansion. Reachable. | +| `compiler/ml/syntaxerr.ml` | `Variable_in_scope` | ⚠ | — | Reachable via `let f: type t. (t, 't) => t = …` (locally-abstract `t` collides with type variable `'t` during `varify_constructors`), but `Syntaxerr.error` has no registered pretty-printer, so it propagates as an uncaught `Fatal error: exception Syntaxerr.Error(_)`. The variant is live; the printer is dead. Treat as broken until either the printer is wired up or the variant is removed in favor of a proper diagnostic. | | `compiler/ml/cmt_format.cppo.ml` | `Not_a_typedtree` | ☐ | — | cmt_format.cppo.ml:147. Fires when reading a `.cmt` file that doesn't contain a typed tree. Needs binary `.cmt` manipulation. | | `compiler/ext/bsc_args.ml` | `Unknown` | ☐ | — | bsc_args.ml:45. Unknown CLI flag passed to `bsc`. Reachable via `bsc --bogus`. | | `compiler/ext/bsc_args.ml` | `Missing` | ☐ | — | Required CLI flag argument missing (e.g. `bsc -o` with no following filename). | @@ -415,20 +415,29 @@ Source: [ast_utf8_string.ml:25](../compiler/frontend/ast_utf8_string.ml). All va | `Invalid_unicode_escape` | ⚠ Dead | | `Invalid_unicode_codepoint_escape` | ⚠ Dead | -## `compiler/frontend/ast_utf8_string_interp.ml` +## `compiler/frontend/ast_utf8_string_interp.ml` (dead family) Source: [ast_utf8_string_interp.ml:25](../compiler/frontend/ast_utf8_string_interp.ml). -| Variant | Status | Fixture | Notes | -|---|---|---|---| -| `Invalid_code_point` | ☐ | — | | -| `Unterminated_backslash` | ☐ | — | | -| `Invalid_escape_code` | ☐ | — | | -| `Invalid_hex_escape` | ☐ | — | | -| `Invalid_unicode_escape` | ☐ | — | | -| `Unterminated_variable` | ☐ | — | | -| `Unmatched_paren` | ☐ | — | | -| `Invalid_syntax_of_var` | ☐ | — | | +`pos_error` is reached only through `check_and_transform`, whose only +caller in `compiler/` is `transform_test` — used by OUnit tests, not the +production pipeline. Modern ReScript backtick templates take the +`BackQuotes` branch of `transform_exp` (line 311) and skip the +interpolation parser entirely. The legacy `{j|…|j}` delimiter the +parser would otherwise route here is no longer accepted by the +scanner. All variants below are unreachable from regular ReScript +source. + +| Variant | Status | +|---|---| +| `Invalid_code_point` | ⚠ Dead | +| `Unterminated_backslash` | ⚠ Dead | +| `Invalid_escape_code` | ⚠ Dead | +| `Invalid_hex_escape` | ⚠ Dead | +| `Invalid_unicode_escape` | ⚠ Dead | +| `Unterminated_variable` | ⚠ Dead | +| `Unmatched_paren` | ⚠ Dead | +| `Invalid_syntax_of_var` | ⚠ Dead | --- diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneBigint.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneBigint.res.expected new file mode 100644 index 0000000000..6575292d23 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneBigint.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneBigint.res:2:20-30 + + 1 │ @unboxed + 2 │ type t = A(bigint) | B(bigint) + 3 │ + + This untagged variant definition is invalid: At most one case can be a bigint type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneBoolean.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneBoolean.res.expected new file mode 100644 index 0000000000..7458940150 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneBoolean.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneBoolean.res:2:18-26 + + 1 │ @unboxed + 2 │ type t = A(bool) | B(bool) + 3 │ + + This untagged variant definition is invalid: At most one case can be a boolean type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneFunction.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneFunction.res.expected new file mode 100644 index 0000000000..439f9c17de --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneFunction.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneFunction.res:2:24-44 + + 1 │ @unboxed + 2 │ type t = A(int => int) | B(string => string) + 3 │ + + This untagged variant definition is invalid: At most one case can be a function type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected new file mode 100644 index 0000000000..841902fcbd --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneInstance.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneInstance.res:2:20-30 + + 1 │ @unboxed + 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 diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneNumber.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneNumber.res.expected new file mode 100644 index 0000000000..cf28363354 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneNumber.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneNumber.res:2:17-26 + + 1 │ @unboxed + 2 │ type t = A(int) | B(float) + 3 │ + + This untagged variant definition is invalid: At most one case can be a number type (int or float). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneObject.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneObject.res.expected new file mode 100644 index 0000000000..c1a32783d8 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneObject.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneObject.res:4:15-20 + + 2 │ type b = {y: int} + 3 │ @unboxed + 4 │ type t = A(a) | B(b) + 5 │ + + This untagged variant definition is invalid: At most one case can be an object type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedConstructorMoreThanOneArg.res.expected b/tests/build_tests/super_errors/expected/UntaggedConstructorMoreThanOneArg.res.expected new file mode 100644 index 0000000000..585544c428 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedConstructorMoreThanOneArg.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedConstructorMoreThanOneArg.res:2:1-32 + + 1 │ @unboxed + 2 │ type t = A(int, int) | B(string) + 3 │ + + This untagged variant definition is invalid: Constructor A has more than one argument. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedDuplicateLiteral.res.expected b/tests/build_tests/super_errors/expected/UntaggedDuplicateLiteral.res.expected new file mode 100644 index 0000000000..61bf9dcad3 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedDuplicateLiteral.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedDuplicateLiteral.res:2:23-34 + + 1 │ @unboxed + 2 │ type t = | @as("x") A | @as("x") B + 3 │ + + This untagged variant definition is invalid: Duplicate literal x. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedDuplicatedBsAs.res.expected b/tests/build_tests/super_errors/expected/UntaggedDuplicatedBsAs.res.expected new file mode 100644 index 0000000000..c986d7dd34 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedDuplicatedBsAs.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedDuplicatedBsAs.res:2:21-23 + + 1 │ @unboxed + 2 │ type t = | @as("x") @as("y") A | B + 3 │ + + duplicate @as \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedInvalidVariantAsAnnotation.res.expected b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantAsAnnotation.res.expected new file mode 100644 index 0000000000..4eb46507df --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantAsAnnotation.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/UntaggedInvalidVariantAsAnnotation.res:1:12-14 + + 1 │ type t = | @as(foo) A | B + 2 │ + + A variant case annotation @as(...) must be a string or integer, boolean, null, undefined \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected new file mode 100644 index 0000000000..897fc145ea --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedInvalidVariantTagAnnotation.res:1:1-4 + + 1 │ @tag(123) + 2 │ type t = | A({foo: int}) | B({bar: string}) + 3 │ + + A variant tag annotation @tag(...) must be a string \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedOnlyOneUnknown.res.expected b/tests/build_tests/super_errors/expected/UntaggedOnlyOneUnknown.res.expected new file mode 100644 index 0000000000..1406dd58d8 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedOnlyOneUnknown.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/UntaggedOnlyOneUnknown.res:4:10-18 + + 2 │ + 3 │ @unboxed + 4 │ type t = A(opaque) | B(int) + 5 │ + + This untagged variant definition is invalid: Case A 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/expected/UntaggedTagFieldNameConflict.res.expected b/tests/build_tests/super_errors/expected/UntaggedTagFieldNameConflict.res.expected new file mode 100644 index 0000000000..4a04a0f4fc --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedTagFieldNameConflict.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/UntaggedTagFieldNameConflict.res:3:3-18 + + 1 │ @tag("kind") + 2 │ type t = + 3 │ | A({kind: int}) + 4 │ | B({other: string}) + 5 │ + + Constructor "A": the @tag name "kind" conflicts with the runtime value of inline record field "kind". Use a different @tag name or rename the field. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/typemod_signature_expected.res.expected b/tests/build_tests/super_errors/expected/typemod_signature_expected.res.expected new file mode 100644 index 0000000000..b709aa9750 --- /dev/null +++ b/tests/build_tests/super_errors/expected/typemod_signature_expected.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/typemod_signature_expected.res:4:18-38 + + 2 │ module M: (X: {}) => {} + 3 │ } + 4 │ module type T2 = T with type M.t = int + 5 │ + + This module type is not a signature \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBigint.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBigint.res new file mode 100644 index 0000000000..8162d2398a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBigint.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(bigint) | B(bigint) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBoolean.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBoolean.res new file mode 100644 index 0000000000..7a78734d25 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneBoolean.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(bool) | B(bool) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneFunction.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneFunction.res new file mode 100644 index 0000000000..42ae1f810f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneFunction.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(int => int) | B(string => string) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneInstance.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneInstance.res new file mode 100644 index 0000000000..1df14c658d --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneInstance.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(Date.t) | B(Date.t) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneNumber.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneNumber.res new file mode 100644 index 0000000000..7cef4afa40 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneNumber.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(int) | B(float) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneObject.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneObject.res new file mode 100644 index 0000000000..f771b6239e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneObject.res @@ -0,0 +1,4 @@ +type a = {x: int} +type b = {y: int} +@unboxed +type t = A(a) | B(b) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedConstructorMoreThanOneArg.res b/tests/build_tests/super_errors/fixtures/UntaggedConstructorMoreThanOneArg.res new file mode 100644 index 0000000000..fd987b0d32 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedConstructorMoreThanOneArg.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(int, int) | B(string) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedDuplicateLiteral.res b/tests/build_tests/super_errors/fixtures/UntaggedDuplicateLiteral.res new file mode 100644 index 0000000000..22e0d23861 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedDuplicateLiteral.res @@ -0,0 +1,2 @@ +@unboxed +type t = | @as("x") A | @as("x") B diff --git a/tests/build_tests/super_errors/fixtures/UntaggedDuplicatedBsAs.res b/tests/build_tests/super_errors/fixtures/UntaggedDuplicatedBsAs.res new file mode 100644 index 0000000000..1bff00d725 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedDuplicatedBsAs.res @@ -0,0 +1,2 @@ +@unboxed +type t = | @as("x") @as("y") A | B diff --git a/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantAsAnnotation.res b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantAsAnnotation.res new file mode 100644 index 0000000000..1bb9457562 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantAsAnnotation.res @@ -0,0 +1 @@ +type t = | @as(foo) A | B diff --git a/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res new file mode 100644 index 0000000000..37a7d19e88 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res @@ -0,0 +1,2 @@ +@tag(123) +type t = | A({foo: int}) | B({bar: string}) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedOnlyOneUnknown.res b/tests/build_tests/super_errors/fixtures/UntaggedOnlyOneUnknown.res new file mode 100644 index 0000000000..22756e6e7a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedOnlyOneUnknown.res @@ -0,0 +1,4 @@ +type opaque + +@unboxed +type t = A(opaque) | B(int) diff --git a/tests/build_tests/super_errors/fixtures/UntaggedTagFieldNameConflict.res b/tests/build_tests/super_errors/fixtures/UntaggedTagFieldNameConflict.res new file mode 100644 index 0000000000..f89e40319f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedTagFieldNameConflict.res @@ -0,0 +1,4 @@ +@tag("kind") +type t = + | A({kind: int}) + | B({other: string}) diff --git a/tests/build_tests/super_errors/fixtures/typemod_signature_expected.res b/tests/build_tests/super_errors/fixtures/typemod_signature_expected.res new file mode 100644 index 0000000000..3f19e510ed --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/typemod_signature_expected.res @@ -0,0 +1,4 @@ +module type T = { + module M: (X: {}) => {} +} +module type T2 = T with type M.t = int From f8a34549877dc4f09f7330441437d3431961dd54 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:46:34 +0200 Subject: [PATCH 09/17] super_errors: cover bs_syntaxerr Invalid_underscore_type_in_external `@obj external make: (~x: _) => _ = ""` triggers the error at optional-label position when no @as is supplied. Also marks typedecl.Val_in_structure as dead: the only way to reach it is via manually-constructed AST, since the parser's external-recovery path emits a syntax error before the typechecker ever sees the value declaration. --- tests/ERROR_VARIANTS.md | 6 +++--- .../bs_invalid_underscore_type_in_external.res.expected | 8 ++++++++ .../fixtures/bs_invalid_underscore_type_in_external.res | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/bs_invalid_underscore_type_in_external.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/bs_invalid_underscore_type_in_external.res diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 37db126679..fd09440062 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -148,7 +148,7 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Bad_fixed_type` | ? | — | typedecl.ml:190/193. `set_fixed_row` runs when `is_fixed_type` returns true — requires an open object `{..f: t}` or open polyvariant `[> #A]` as `ptype_manifest`. Then if the expanded head isn't `Tvariant` / `Tobject` (line 190) or the row variable isn't `Tvar` (line 193), error. Reachable in principle via an alias chain that collapses the open row, but I haven't constructed one. | | `Unbound_type_var_ext` | ✓ | `unbound_type_var_extension.res` | | | `Varying_anonymous` | ? | — | typedecl.ml:1263. Requires anonymous constrained type params under specific variance; very obscure but trigger site is live. | -| `Val_in_structure` | ? | — | typedecl.ml:1887. Requires `pval_prim = []` for an external. Parser emits at least one string for any external; `[]` would only come from PPX or manual AST construction. Probably effectively dead. | +| `Val_in_structure` | ⚠ | — | typedecl.ml:1887 requires `pval_prim = []` outside a signature. The parser's `external` recovery sets `prim = []` (`res_core.ml:6617`) but only after emitting a `Syntax error`, so the typechecker never reaches the value declaration. From plain source there's no path that produces a non-signature `Val` with empty `pval_prim` — only PPX-rewritten AST could, and the AST shape would have to bypass the parser. | | `Invalid_attribute` | ✓ | `invalid_attribute_not_undefined.res` | | | `Bad_immediate_attribute` | ✓ | `bad_immediate_attribute.res` | | | `Bad_unboxed_attribute` | ✓ | `bad_unboxed_attribute_abstract.res`, `bad_unboxed_attribute_mutable.res`, `bad_unboxed_attribute_many_fields.res`, `bad_unboxed_attribute_extensible.res` | All 4 sub-cases covered. | @@ -168,7 +168,7 @@ Module-level errors. Source: [typemod.ml:24](../compiler/ml/typemod.ml). | `Cannot_apply` | ✓ | `cannot_apply_non_functor.res` | | | `Not_included` | ✓ | All `super_errors_multi/Iface_*` fixtures wrap to this via `compunit`. | | | `Cannot_eliminate_dependency` | ☐ | — | typemod.ml:1335. Requires anonymous functor application whose result still mentions the bound module; couldn't engineer despite multiple attempts. May be effectively dead — every fixture's `nondep_supertype` succeeded with existential substitution. | -| `Signature_expected` | ☐ | — | typemod.ml:78, 1184. Extract-sig on non-signature module type. | +| `Signature_expected` | ✓ | `typemod_signature_expected.res` | `with type M.t = …` where `M` is functor-typed inside the outer signature. | | `Structure_expected` | ✓ | `super_errors_multi/Smoke_unbound_module_reference` (indirect); also `open_functor.res` | | | `With_no_component` | ✓ | `with_no_component.res` | | | `With_mismatch` | ✓ | `with_mismatch.res` | | @@ -286,7 +286,7 @@ FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../c | `Expect_string_literal` | ✓ | `bs_expect_string_literal.res` | | | `Expect_int_or_string_or_json_literal` | ✓ | `bs_expect_int_or_string_or_json_literal.res` | `@as(true)` on a wildcard external argument. | | `Unhandled_poly_type` | ? | — | ast_core_type.ml:141. Triggers in `list_of_arrow` when an arrow chain contains a `Ptyp_poly`. The parser doesn't normally produce inline poly types inside arrows, but record fields can have polytypes that flow through these utilities. | -| `Invalid_underscore_type_in_external` | ? | — | ast_external_process.ml:107/132. Needs `_` in optional-label external position with no `@as`. Probably reachable in `@@obj` externals; not yet verified. | +| `Invalid_underscore_type_in_external` | ✓ | `bs_invalid_underscore_type_in_external.res` | `@obj external make: (~x: _) => _ = ""` — `_` at an optional-label position without `@as`. | | `Invalid_bs_string_type` | ✓ | `bs_invalid_bs_string_type.res` | | | `Invalid_bs_int_type` | ✓ | `bs_invalid_bs_int_type.res` | | | `Invalid_bs_unwrap_type` | ✓ | `bs_invalid_bs_unwrap_type.res` | | diff --git a/tests/build_tests/super_errors/expected/bs_invalid_underscore_type_in_external.res.expected b/tests/build_tests/super_errors/expected/bs_invalid_underscore_type_in_external.res.expected new file mode 100644 index 0000000000..12ddf90961 --- /dev/null +++ b/tests/build_tests/super_errors/expected/bs_invalid_underscore_type_in_external.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/bs_invalid_underscore_type_in_external.res:1:26 + + 1 │ @obj external make: (~x: _) => _ = "" + 2 │ + + _ is not allowed in combination with external optional type \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/bs_invalid_underscore_type_in_external.res b/tests/build_tests/super_errors/fixtures/bs_invalid_underscore_type_in_external.res new file mode 100644 index 0000000000..96c5a0c218 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/bs_invalid_underscore_type_in_external.res @@ -0,0 +1 @@ +@obj external make: (~x: _) => _ = "" From a5ddb00169b34ecc4e9a14b7cfd921baab27b0c6 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:48:37 +0200 Subject: [PATCH 10/17] super_errors: cover typedecl Extension_mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding `type t += A(int)` to a parametric extensible type `type t<'a> = ..` triggers Extension_mismatch with an Arity sub-error. Single-file fixture suffices — the variant fires inside `transl_type_extension` regardless of cross-module context. --- tests/ERROR_VARIANTS.md | 2 +- .../expected/extension_arity_mismatch.res.expected | 11 +++++++++++ .../fixtures/extension_arity_mismatch.res | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/build_tests/super_errors/expected/extension_arity_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/extension_arity_mismatch.res diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index fd09440062..0bc7de9869 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -139,7 +139,7 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Unbound_type_var` | ✓ | `unbound_type_var.res` | | | `Cannot_extend_private_type` | ✓ | `cannot_extend_private_type.res` | | | `Not_extensible_type` | ✓ | `not_extensible_type.res` | | -| `Extension_mismatch` | ☐ | — | Cross-module extension declaration mismatch via `.resi`/`.res`. | +| `Extension_mismatch` | ✓ | `extension_arity_mismatch.res` | `type t<'a> = ..` extended with `type t += A(int)` — arity differs from the extensible type. | | `Rebind_wrong_type` | ? | — | typedecl.ml:1653. Fires when source constructor's result type doesn't unify with target's. For exceptions both are `exn`; for extension types both share the extensible parent. I couldn't construct a triggering shape — the rebind succeeds for shapes the parser will accept. | | `Rebind_mismatch` | ✓ | `extension_rebind_mismatch.res` | Rebinding constructor into a different extensible type. | | `Rebind_private` | ✓ | `extension_rebind_private.res` | Rebinding a private extension constructor as public. | diff --git a/tests/build_tests/super_errors/expected/extension_arity_mismatch.res.expected b/tests/build_tests/super_errors/expected/extension_arity_mismatch.res.expected new file mode 100644 index 0000000000..6c22fed77c --- /dev/null +++ b/tests/build_tests/super_errors/expected/extension_arity_mismatch.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/extension_arity_mismatch.res:3:1-16 + + 1 │ type t<'a> = .. + 2 │ + 3 │ type t += A(int) + 4 │ + + This extension does not match the definition of type t + They have different arities. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/extension_arity_mismatch.res b/tests/build_tests/super_errors/fixtures/extension_arity_mismatch.res new file mode 100644 index 0000000000..aab2fdaf7f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/extension_arity_mismatch.res @@ -0,0 +1,3 @@ +type t<'a> = .. + +type t += A(int) From abb7840d51c24e5f33bc29a4a08f678924f04389 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:52:11 +0200 Subject: [PATCH 11/17] ERROR_VARIANTS: document harness-bound and PPX-only variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark each remaining ☐ row with the specific harness it would need ("needs build harness", "needs binary harness", "needs CLI harness", "needs PPX harness") so the catalog distinguishes "reachable but blocked by the source-only harnesses" from "reachable and just unwritten". Also extend the Confirmed-dead section with the new findings: Conflict_bs_bs_this_bs_meth (never raised), ast_utf8_string_interp.* (only called from OUnit transform_test), Val_in_structure (parser recovery emits syntax error first), Illegal_value_name (parser never produces "->" or #-containing idents), and warnings 10/16/108 (each blocked by a specific code path). --- tests/ERROR_VARIANTS.md | 91 +++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 0bc7de9869..c20df6fdd5 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -339,16 +339,16 @@ Build / dependency errors. Mostly need the `rescript build` runtime to fire — | Variant | Status | Fixture | Notes | |---|---|---|---| -| `Cmj_not_found` | ☐ | — | Missing `.cmj` from a dependent module. Needs `rescript build` harness. | +| `Cmj_not_found` | ☐ (needs build harness) | — | Missing `.cmj` from a dependent module. Reachable from `rescript build` but not from raw `bsc`. | | `Js_not_found` | ✓ | implicitly — bypassed via `-bs-cmi-only` in `super_errors_multi` runner. Not a fixture, but the harness commit documents the workaround. | | -| `Bs_cyclic_depends` | ☐ | — | Cycle across compilation units; needs build-system harness. | -| `Bs_duplicated_module` | ☐ | — | Same module name in two source paths. | -| `Bs_duplicate_exports` | ☐ | — | Same export emitted twice; depends/build setup needed. | -| `Bs_package_not_found` | ☐ | — | `rescript.json`-referenced package not resolvable. | -| `Bs_main_not_exist` | ☐ | — | `rescript.json` `main` entry missing. | -| `Bs_invalid_path` | ☐ | — | `-I` / source path with invalid form. | -| `Missing_ml_dependency` | ☐ | — | Compile-time missing dependency. | -| `Dependency_script_module_dependent_not` | ☐ | — | `js_name_of_module_id.cppo.ml:122`. **Reachable** when a dependent module is in script mode (`Package_script`) but the current module is in package mode (`Package_found _`). Legacy script-vs-package interaction; needs `rescript.json` harness. | +| `Bs_cyclic_depends` | ☐ (needs build harness) | — | Cycle across compilation units; the dependency graph that detects this is owned by `rewatch` / `bsb`, not raw `bsc`. | +| `Bs_duplicated_module` | ☐ (needs build harness) | — | Same module name in two source paths under a single package. | +| `Bs_duplicate_exports` | ☐ (needs build harness) | — | Same export emitted twice across compilation units. | +| `Bs_package_not_found` | ☐ (needs build harness) | — | `rescript.json`-referenced package not resolvable. | +| `Bs_main_not_exist` | ☐ (needs build harness) | — | `rescript.json` `main` entry missing. | +| `Bs_invalid_path` | ☐ (needs build harness) | — | `-I` / source path with invalid form. | +| `Missing_ml_dependency` | ☐ (needs build harness) | — | Compile-time missing dependency from a `.cmj` lookup table. | +| `Dependency_script_module_dependent_not` | ☐ (needs build harness) | — | `js_name_of_module_id.cppo.ml:122`. **Reachable** when a dependent module is in script mode (`Package_script`) but the current module is in package mode (`Package_found _`). Legacy script-vs-package interaction; needs `rescript.json` harness. | --- @@ -358,33 +358,40 @@ Environment / `.cmi`-consistency errors. Source: [env.ml:57](../compiler/ml/env. | Variant | Status | Fixture | Notes | |---|---|---|---| -| `Illegal_renaming` | ☐ | — | `.cmi` filename doesn't match module name; multi-unit scenario. | -| `Inconsistent_import` | ☐ | — | Two `.cmi` files disagree on a type's hash; needs synthetic build state. | -| `Missing_module` | ☐ | — | `.cmi` not findable when referenced; needs multi-file harness. | -| `Illegal_value_name` | ☐ | — | Reserved identifier name; very specific. | +| `Illegal_renaming` | ☐ (needs build harness) | — | Triggered when a `.cmi` filename and the module name inside it disagree. Reachable via `rescript.json` setups that rename the produced artefact, but not from a single-process `bsc` invocation that always writes `Module.cmi` to match the source. | +| `Inconsistent_import` | ☐ (needs build harness) | — | Triggered when two `.cmi` files transitively imported by the same unit declare different CRCs for the same type. Needs an artificially-mutated build state across multiple compile invocations. | +| `Missing_module` | ☐ (needs build harness) | — | `.cmi` referenced but absent from `-I` paths at compile time. The `super_errors_multi` runner pre-compiles every fixture file via `-bs-read-cmi`, so it never reaches this code path. | +| `Illegal_value_name` | ⚠ | — | env.ml:1622/1625 raises when an identifier is `"->"` or starts/contains `#`. The ReScript parser never emits such identifiers; only PPX-rewritten AST could reach the check. | --- ## `compiler/ml/cmi_format.ml` -`.cmi` file format errors. Need binary-level manipulation to trigger. +`.cmi` file format errors. Need binary-level manipulation to trigger +(write a non-`.cmi` file in place of one and reference it; downgrade +compiler version; truncate the file). Out of scope for the +`super_errors{,_multi}` harnesses, which only invoke `bsc` on +hand-written `.res` / `.resi` sources. | Variant | Status | Fixture | Notes | |---|---|---|---| -| `Not_an_interface` | ☐ | — | Pass an arbitrary file as `.cmi`. | -| `Wrong_version_interface` | ☐ | — | Mismatched compiler versions writing/reading. | -| `Corrupted_interface` | ☐ | — | Truncated or corrupted `.cmi`. | +| `Not_an_interface` | ☐ (needs binary harness) | — | Pass an arbitrary file as `.cmi`. | +| `Wrong_version_interface` | ☐ (needs binary harness) | — | Mismatched compiler versions writing/reading. | +| `Corrupted_interface` | ☐ (needs binary harness) | — | Truncated or corrupted `.cmi`. | --- ## `compiler/core/cmd_ast_exception.ml` PPX-runtime errors. Source: [cmd_ast_exception.ml:24](../compiler/core/cmd_ast_exception.ml). +Both require running `bsc` with `-ppx ` and exercising the +external process boundary. Not reachable from the single-file or +multi-file harnesses, which never set `-ppx`. | Variant | Status | Fixture | Notes | |---|---|---|---| -| `CannotRun` | ☐ | — | PPX binary fails to execute. | -| `WrongMagic` | ☐ | — | PPX returns wrong AST magic number. | +| `CannotRun` | ☐ (needs PPX harness) | — | PPX binary fails to execute (missing or non-executable). | +| `WrongMagic` | ☐ (needs PPX harness) | — | PPX returns wrong AST magic number (e.g. PPX built against a different compiler ABI). | --- @@ -397,9 +404,9 @@ PPX-runtime errors. Source: [cmd_ast_exception.ml:24](../compiler/core/cmd_ast_e | `compiler/ml/transl_recmodule.ml` | `Circular_dependency` | ✓ | `recmodule_circular_dependency.res` | | | `compiler/ml/rec_check.ml` | `Illegal_letrec_expr` | ✓ | `illegal_letrec_expr.res` | | | `compiler/ml/syntaxerr.ml` | `Variable_in_scope` | ⚠ | — | Reachable via `let f: type t. (t, 't) => t = …` (locally-abstract `t` collides with type variable `'t` during `varify_constructors`), but `Syntaxerr.error` has no registered pretty-printer, so it propagates as an uncaught `Fatal error: exception Syntaxerr.Error(_)`. The variant is live; the printer is dead. Treat as broken until either the printer is wired up or the variant is removed in favor of a proper diagnostic. | -| `compiler/ml/cmt_format.cppo.ml` | `Not_a_typedtree` | ☐ | — | cmt_format.cppo.ml:147. Fires when reading a `.cmt` file that doesn't contain a typed tree. Needs binary `.cmt` manipulation. | -| `compiler/ext/bsc_args.ml` | `Unknown` | ☐ | — | bsc_args.ml:45. Unknown CLI flag passed to `bsc`. Reachable via `bsc --bogus`. | -| `compiler/ext/bsc_args.ml` | `Missing` | ☐ | — | Required CLI flag argument missing (e.g. `bsc -o` with no following filename). | +| `compiler/ml/cmt_format.cppo.ml` | `Not_a_typedtree` | ☐ (needs binary harness) | — | cmt_format.cppo.ml:147. Fires when a tool reads a `.cmt` file whose first block isn't a typed tree. Reachable in principle by pointing the analyzer at an arbitrary file with a `.cmt` extension; out of scope for the source-only fixture harnesses. | +| `compiler/ext/bsc_args.ml` | `Unknown` | ☐ (needs CLI harness) | — | bsc_args.ml:45. Reachable trivially via `bsc --bogus`, but the `super_errors{,_multi}` runners only pass `bsc` a fixed flag list plus the source file — they can't exercise CLI-level errors. | +| `compiler/ext/bsc_args.ml` | `Missing` | ☐ (needs CLI harness) | — | Same as above: `bsc -o` (no following filename). Needs a harness that invokes `bsc` with crafted argv. | --- @@ -462,6 +469,9 @@ be live and just hard to reproduce. - `typecore.Invalid_for_of_pattern` — parser's `normalize_for_of_pattern` (`res_core.ml:3841`) replaces every non-var, non-`_` pattern with `Ppat_any` before the typer runs. +- `bs_syntaxerr.Conflict_bs_bs_this_bs_meth` — variant is declared but + no `Bs_syntaxerr.err _ Conflict_bs_bs_this_bs_meth` call exists in + `compiler/`. **Verified dead because parser doesn't produce required AST shape:** @@ -482,15 +492,42 @@ be live and just hard to reproduce. (`res_scanner.ml:350-417`) already validates escape sequences and unicode code points; the transform never sees a string that would fail its own re-validation. +- `ast_utf8_string_interp.*` (the whole module's error variants) — + `check_and_transform` is only ever called from `transform_test`, which + exists for OUnit tests, not the production pipeline. Modern ReScript + backtick templates take the `BackQuotes` branch of `transform_exp` + and skip the interpolation parser entirely. +- `typedecl.Val_in_structure` — typedecl.ml:1887 requires `pval_prim + = []` outside a signature; the parser's `external` recovery sets + `prim = []` only after emitting a syntax error, so the typechecker + never reaches the value declaration. +- `env.Illegal_value_name` — env.ml:1622/1625 rejects `"->"` and + identifiers containing `#`. The parser never produces such names; + PPX-rewritten AST is the only path that could trigger it. +- `bs_warnings.Statement_type` (warning 10) — only caller of + `check_application_result` passes `statement = false`, so the + `if statement then …` branch never fires. +- `bs_warnings.Unerasable_optional_argument` (warning 16) — + `type_function` (typecore.ml:3479) explicitly disables this warning + via `Warnings.parse_options false "-16"` before the check runs. +- `bs_warnings.Bs_uninterpreted_delimiters` (warning 108) — raised at + `bs_warnings.ml:29` for `Pconst_string` with delimiter `"js"`, but + the modern scanner has no `{js|…|js}` form and template strings tag + with `"bq"` after rewriting. + +**`Syntaxerr.Variable_in_scope` is a special case** — reachable from +`let f: type t. (t, 't) => t = …` but raised without a registered +printer, so it surfaces as `Fatal error: exception Syntaxerr.Error(_)`. +The variant is live; the diagnostic path is broken. Fix should either +wire up a printer or convert the check into a regular typed error. **Probably dead but not formally verified** (`?` in tables above; needs -deeper analysis before removal): `Polymorphic_label`, +deeper analysis before removal): `Polymorphic_label`, `Label_mismatch`, `Abstract_wrong_label`, `Incoherent_label_order`, `Parameters_differ`, -`Bad_fixed_type`, `Varying_anonymous`, `Val_in_structure`, -`Unbound_type_constructor_2`, `Cannot_quantify`, -`Present_has_conjunction`, `Present_has_no_type`, +`Bad_fixed_type`, `Varying_anonymous`, `Unbound_type_constructor_2`, +`Cannot_quantify`, `Present_has_conjunction`, `Present_has_no_type`, `With_cannot_remove_constrained_type`, `Unhandled_poly_type`, -`Invalid_underscore_type_in_external`. +`Rebind_wrong_type`, `Type_clash`. --- From 788e8a353eb212c7b76732102fb8a872cf810095 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:55:16 +0200 Subject: [PATCH 12/17] =?UTF-8?q?ERROR=5FVARIANTS:=20classify=20remaining?= =?UTF-8?q?=20=E2=98=90=20rows=20by=20required=20harness=20or=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every remaining ☐ row now carries a parenthetical tag ("needs build harness" / "needs binary harness" / "needs CLI harness" / "needs PPX harness" / "needs harness flag") so a reader can tell at a glance whether a variant is unwritten coverage vs. blocked by what the harness invokes bsc with. Promotes Label_mismatch, Type_clash, Cannot_eliminate_dependency, Non_generalizable_module, and Illegal_reference_to_recursive_module from ☐ to ? — they have live raise sites but every attempted reproduction hit a different error first. Marks With_makes_applicative_functor_ill_typed and includemod.Unbound_modtype_path as dead with the source-level reason. --- tests/ERROR_VARIANTS.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index c20df6fdd5..69d290c0ce 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -66,7 +66,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). |---|---|---|---| | `Polymorphic_label` | ? | — | typecore.ml:1555. Triggers in record-pattern matching when a polymorphic field gets instantiated. Several `'a. 'a => 'a` record-field reproductions compiled cleanly; the trigger site is live but I couldn't find an AST that reaches it. | | `Constructor_arity_mismatch` | ✓ | `constructor_arity_mismatch.res`, `constructor_arity_mismatch_pattern.res`, `arity_mismatch*.res` | Triggers in both expression (4028) and pattern (1426) paths. | -| `Label_mismatch` | ☐ | — | typecore.ml:3589. Record label type clash with explicit unify failure; often subsumed by `Pattern_type_clash` / `Expr_type_clash`. | +| `Label_mismatch` | ? | — | typecore.ml:1543/3589. Fires when `unify ty_res ty_expected` fails after a label has been disambiguated. In practice the disambiguator (`type_label_a_list` / `type_label_pat`) locks the record type before this unify runs, so the error is subsumed by `Wrong_name` / `Pattern_type_clash` / `Expr_type_clash`. I couldn't construct a fixture that reaches it. | | `Pattern_type_clash` | ✓ | many `*_pattern_type_clash.res` etc. | Most-fired pattern error; covered through many fixtures but report-side sub-cases (option-vs-non-option trace, polyvariant context, etc.) remain partly untested. | | `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | | `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | @@ -114,7 +114,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Field_not_optional` | ✓ | `fieldNotOptional.res` | | | `Type_params_not_supported` | ✓ | `variant_spread_pattern_type_params.res` | Pattern-level variant spread (`| ...a as v`) where `a` has type params; typedecl path covered by `variant_spread_type_parameters.res`. | | `Field_access_on_dict_type` | ✓ | `field_access_on_dict_type.res` | | -| `Jsx_not_enabled` | ☐ | — | typecore.ml:218/3470. Fires when JSX expressions are used without `-bs-jsx N`. Reachable but the existing `super_errors` runner always passes `-bs-jsx 4`. | +| `Jsx_not_enabled` | ☐ (needs harness flag) | — | typecore.ml:218/3470. Fires when JSX is used without `-bs-jsx N`. The `super_errors` runner hard-codes `-bs-jsx 4` in `bscFlags`; adding a per-fixture opt-out (e.g. a `.opts` sidecar) would expose this. Until then, it's reachable in real code but blocked at the harness level. | --- @@ -133,7 +133,7 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Definition_mismatch` | ✓ | `definition_mismatch.res` | | | `Constraint_failed` | ✓ | `constraint_failed.res` | | | `Inconsistent_constraint` | ✓ | `inconsistent_constraint.res` | | -| `Type_clash` | ☐ | — | typedecl.ml:125. Manifest type doesn't unify with kind. | +| `Type_clash` | ? | — | typedecl.ml:125. Fires in `update_type` during recursive type elaboration when the manifest of a recursive `type` doesn't unify with the placeholder `newconstr path params`. Every attempted reproduction hits `Cycle_in_def` or `Recursive_abbrev` first. | | `Parameters_differ` | ? | — | typedecl.ml:988. Non-uniform recursive type abbreviation; ReScript variant recursion is accepted, and abbreviations cycle to `Cycle_in_def` first. Hard to construct a reproduction that lands here exactly. | | `Null_arity_external` | ⚠ | — | typedecl.ml:1900. The guard requires `prim_arity = 0` and `prim_native_name` not having the magic 20-byte encoding (`\132\149...`) and `prim_name` not starting with `%` or `#`. The encoding gets applied to every concrete external by `Primitive.parse_declaration`, and empty `prim_name` is rejected earlier by `external_ffi_types.ml` with "Not a valid global name". No path through the parser reaches it. | | `Unbound_type_var` | ✓ | `unbound_type_var.res` | | @@ -144,7 +144,7 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Rebind_mismatch` | ✓ | `extension_rebind_mismatch.res` | Rebinding constructor into a different extensible type. | | `Rebind_private` | ✓ | `extension_rebind_private.res` | Rebinding a private extension constructor as public. | | `Bad_variance` | ✓ | `bad_variance.res`, `bad_variance_contra.res` | | -| `Unavailable_type_constructor` | ☐ | — | typedecl.ml:778. Requires a type path findable at parse time but missing during constraint enforcement; only cross-unit scenarios. | +| `Unavailable_type_constructor` | ☐ (needs build harness) | — | typedecl.ml:778. Requires a type path findable at parse time but missing during constraint enforcement; only cross-unit scenarios where a `.cmi` was found but later removed. | | `Bad_fixed_type` | ? | — | typedecl.ml:190/193. `set_fixed_row` runs when `is_fixed_type` returns true — requires an open object `{..f: t}` or open polyvariant `[> #A]` as `ptype_manifest`. Then if the expanded head isn't `Tvariant` / `Tobject` (line 190) or the row variable isn't `Tvar` (line 193), error. Reachable in principle via an alias chain that collapses the open row, but I haven't constructed one. | | `Unbound_type_var_ext` | ✓ | `unbound_type_var_extension.res` | | | `Varying_anonymous` | ? | — | typedecl.ml:1263. Requires anonymous constrained type params under specific variance; very obscure but trigger site is live. | @@ -167,17 +167,17 @@ Module-level errors. Source: [typemod.ml:24](../compiler/ml/typemod.ml). |---|---|---|---| | `Cannot_apply` | ✓ | `cannot_apply_non_functor.res` | | | `Not_included` | ✓ | All `super_errors_multi/Iface_*` fixtures wrap to this via `compunit`. | | -| `Cannot_eliminate_dependency` | ☐ | — | typemod.ml:1335. Requires anonymous functor application whose result still mentions the bound module; couldn't engineer despite multiple attempts. May be effectively dead — every fixture's `nondep_supertype` succeeded with existential substitution. | +| `Cannot_eliminate_dependency` | ? | — | typemod.ml:1335. Requires anonymous functor application whose result still mentions the bound module; couldn't engineer despite multiple attempts. May be effectively dead — every reproduction's `nondep_supertype` succeeded with existential substitution. | | `Signature_expected` | ✓ | `typemod_signature_expected.res` | `with type M.t = …` where `M` is functor-typed inside the outer signature. | | `Structure_expected` | ✓ | `super_errors_multi/Smoke_unbound_module_reference` (indirect); also `open_functor.res` | | | `With_no_component` | ✓ | `with_no_component.res` | | | `With_mismatch` | ✓ | `with_mismatch.res` | | -| `With_makes_applicative_functor_ill_typed` | ☐ | — | typemod.ml:258. Requires applicative-functor constructions ReScript syntax doesn't expose. | -| `With_changes_module_alias` | ☐ | — | typemod.ml:240. Requires `with module = ...` substitution invalidating an aliased path. ReScript may not parse `with module`. | -| `With_cannot_remove_constrained_type` | ? | — | typemod.ml:443. Triggers when destructive substitution `with type X<'a> := T` is applied where the substituted type has constrained type params (non-`Tvar`). One attempted reproduction succeeded; haven't found a triggering shape. | +| `With_makes_applicative_functor_ill_typed` | ⚠ | — | typemod.ml:258. Reached only through the applicative-functor path of `Btype.it_path` (`Papply`); ReScript's parser doesn't emit `Papply` (no parsed construction site in `res_core.ml`), so the iterator never visits this branch. | +| `With_changes_module_alias` | ☐ (needs build harness) | — | typemod.ml:240. Fires during `with module := M2` substitution when an aliased sub-module inside the constrained signature is affected. ReScript parses `with module N := M2` (destructive substitution), but constructing a sub-module alias chain that gets invalidated requires multiple `.resi` files and a specific shape I couldn't reproduce single-file. | +| `With_cannot_remove_constrained_type` | ? | — | typemod.ml:443. Triggers when destructive substitution `with type X<'a> := T` is applied where the substituted type has constrained type params (non-`Tvar`). Reproductions either succeeded outright or hit a different `with`-clause error first. | | `Repeated_name` | ✓ | `repeated_def_*.res` (multiple) | | | `Non_generalizable` | ✓ | `non_generalizable.res` | | -| `Non_generalizable_module` | ☐ | — | typemod.ml:1023. Module value with non-closed type at sealing time; cross-file. | +| `Non_generalizable_module` | ? | — | typemod.ml:1023. Fires when sealing a module whose `md_type` still contains free non-generalisable type variables. Single-file fixtures hit `Non_generalizable` on the value first; the module-level path is in principle reachable via a functor argument with an open row, but I couldn't construct one. | | `Interface_not_compiled` | ✓ | `super_errors_multi/Iface_not_compiled` | | | `Not_allowed_in_functor_body` | ✓ | `super_errors_multi/not_allowed_in_functor_body` (TODO: confirm path) | | | `Not_a_packed_module` | ✓ | `not_a_packed_module.res` | | @@ -185,7 +185,7 @@ Module-level errors. Source: [typemod.ml:24](../compiler/ml/typemod.ml). | `Scoping_pack` | ⚠ | — | typemod.ml:1717. Requires first-class module pack where a constraint type has a level mismatch; very contrived. | | `Recursive_module_require_explicit_type` | ✓ | `recursive_module_require_explicit_type.res` | | | `Apply_generative` | ✓ | `apply_generative.res` | | -| `Cannot_scrape_alias` | ☐ | — | typemod.ml:77, 83, 1347. Requires `Env.scrape_alias` returning `Mty_alias` (alias target's `.cmi` not loaded). Only multi-unit scenarios. | +| `Cannot_scrape_alias` | ☐ (needs build harness) | — | typemod.ml:77, 83, 1347. Requires `Env.scrape_alias` to return `Mty_alias` for an alias whose target `.cmi` couldn't be loaded. The `super_errors_multi` runner pre-compiles every file in the fixture, so the alias target is always present. | --- @@ -216,10 +216,10 @@ Type-expression errors. Source: [typetexp.ml:28](../compiler/ml/typetexp.ml). | `Unbound_module` | ✓ | `suggest_module_for_missing_identifier.res`, `super_errors_multi/Smoke_unbound_module_reference` | | | `Unbound_modtype` | ✓ | `typetexp_unbound_modtype.res` | | | `Ill_typed_functor_application` | ⚠ | — | typetexp.ml:102. In the `Longident.Lapply` branch. **Verified: parser has no construction site for `Longident.Lapply`** (no result in `res_core.ml`). Confirmed dead. | -| `Illegal_reference_to_recursive_module` | ☐ | — | typetexp.ml:75/114. Catches `Env.Recmodule` exception, raised when looking up a module currently being recursively defined (`#recmod#` placeholder, env.ml:1048). Reachable in principle via a recmodule whose signature references another recmodule member's type before sealing; couldn't construct a triggering fixture but trigger sites are live. | +| `Illegal_reference_to_recursive_module` | ? | — | typetexp.ml:75/114. Catches `Env.Recmodule` (env.ml:1048), raised when looking up a module whose `md_type` is the `#recmod#` placeholder. The placeholder is only ever installed during the first pass over `module rec` blocks. Plain self-references through the declared signature don't reach the lookup with the placeholder type — they go through the sealed signature instead. I couldn't find a recmodule shape that reaches the placeholder; the trigger site is live but might be effectively dead. | | `Access_functor_as_structure` | ✓ | `access_functor_as_structure.res` | | | `Apply_structure_as_functor` | ⚠ | — | typetexp.ml:93. In the `Longident.Lapply` branch. Same dead reason as `Ill_typed_functor_application`. | -| `Cannot_scrape_alias` | ☐ | — | typetexp.ml:86 (Ldot path, live), 95/101 (Lapply path, dead since `Lapply` isn't parsed). The Ldot trigger needs `Env.scrape_alias` to return `Mty_alias` — i.e. an alias whose target `.cmi` can't be loaded. Multi-unit only. | +| `Cannot_scrape_alias` | ☐ (needs build harness) | — | typetexp.ml:86 (Ldot path, live), 95/101 (Lapply path, dead since `Lapply` isn't parsed). The live Ldot trigger needs `Env.scrape_alias` to return `Mty_alias` — an alias whose target `.cmi` couldn't be loaded. The `super_errors_multi` harness pre-compiles every alias target. | | `Opened_object` | ✓ | `object_inherit_opened.res` | | | `Not_an_object` | ✓ | `object_inherit_not_an_object.res` | | @@ -239,9 +239,9 @@ Wrapper symptoms attached to inclusion failures. Source: [includemod.ml:23](../c | `Modtype_infos` | ✓ | `super_errors_multi/Iface_modtype_infos` | | | `Modtype_permutation` | ✓ | `super_errors_multi/include_modtype_permutation` | | | `Interface_mismatch` | ✓ | wrapper added to all `Iface_*` failures (line 476). | | -| `Unbound_modtype_path` | ☐ | — | includemod.ml:94. Requires module-type path comparison to fail; only triggers via destructive substitution paths ReScript doesn't expose. | -| `Unbound_module_path` | ☐ | — | includemod.ml:226/233. Alias comparison where `normalize_path` fails. Multi-unit scenarios only. | -| `Invalid_module_alias` | ☐ | — | includemod.ml:211. Requires both sides `Mty_alias` with one pointing to a functor argument. Functor-with-alias-sig fixtures hit `Module_types` instead. | +| `Unbound_modtype_path` | ⚠ | — | includemod.ml:94. Fires inside `modtype_path` comparison when `Env.find_modtype` raises `Not_found`. The only callers run after both signatures have been fully typed, so the module-type path is always findable from the local env — `Not_found` would imply a stale `.cmi`, which the multi-file harness can't produce since it always pre-compiles. Treating as dead from the source-only harnesses' point of view. | +| `Unbound_module_path` | ☐ (needs build harness) | — | includemod.ml:226/233. Alias comparison where `Env.normalize_path` raises `Not_found`. Requires a module alias whose target `.cmi` is absent at inclusion time — multi-unit only. | +| `Invalid_module_alias` | ☐ (needs build harness) | — | includemod.ml:211. Requires both sides `Mty_alias` with one pointing to a functor argument. Reachable only when the alias chain crosses a functor application that the `super_errors_multi` harness doesn't construct. | --- From 8c35656a256cdd1626d1d5c6c4266a9bfdd91315 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 15:59:12 +0200 Subject: [PATCH 13/17] super_errors: cover three Expr_type_clash sub-cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each fixture exercises a distinct trace-shape that the existing catalog notes as untested: StringConcat ("hi" ++ 42 routes through the dedicated string-concat context message), IfReturn (`let x: string = if … { 1 } else { 2 }` swaps the generic "expected to have type" for the if-statement-specific framing), and MaybeUnwrapOption (`let x: int = Some(5)` — option-vs-non-option recognition path). --- .../expected/if_return_type_mismatch.res.expected | 13 +++++++++++++ .../expected/maybe_unwrap_option.res.expected | 9 +++++++++ .../expected/string_concat_non_string.res.expected | 11 +++++++++++ .../fixtures/if_return_type_mismatch.res | 1 + .../super_errors/fixtures/maybe_unwrap_option.res | 1 + .../fixtures/string_concat_non_string.res | 1 + 6 files changed, 36 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/maybe_unwrap_option.res.expected create mode 100644 tests/build_tests/super_errors/expected/string_concat_non_string.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/maybe_unwrap_option.res create mode 100644 tests/build_tests/super_errors/fixtures/string_concat_non_string.res diff --git a/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected new file mode 100644 index 0000000000..31c29263bf --- /dev/null +++ b/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/if_return_type_mismatch.res:1:27 + + 1 │ let x: string = if true { 1 } else { 2 } + 2 │ + + This has type: int + But this if statement is expected to return: string + + if expressions must return the same type in all branches (if, else if, else). + + You can convert int to string with Int.toString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/maybe_unwrap_option.res.expected b/tests/build_tests/super_errors/expected/maybe_unwrap_option.res.expected new file mode 100644 index 0000000000..ddab2a4936 --- /dev/null +++ b/tests/build_tests/super_errors/expected/maybe_unwrap_option.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/maybe_unwrap_option.res:1:14-20 + + 1 │ let x: int = Some(5) + 2 │ + + This has type: option<'a> + But it's expected to have type: int \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/string_concat_non_string.res.expected b/tests/build_tests/super_errors/expected/string_concat_non_string.res.expected new file mode 100644 index 0000000000..b94d1198a4 --- /dev/null +++ b/tests/build_tests/super_errors/expected/string_concat_non_string.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/string_concat_non_string.res:1:17-18 + + 1 │ let x = "hi" ++ 42 + 2 │ + + This has type: int + But string concatenation is expecting: string + + You can convert int to string with Int.toString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res b/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res new file mode 100644 index 0000000000..b9d9d06610 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res @@ -0,0 +1 @@ +let x: string = if true { 1 } else { 2 } diff --git a/tests/build_tests/super_errors/fixtures/maybe_unwrap_option.res b/tests/build_tests/super_errors/fixtures/maybe_unwrap_option.res new file mode 100644 index 0000000000..ef814deefd --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/maybe_unwrap_option.res @@ -0,0 +1 @@ +let x: int = Some(5) diff --git a/tests/build_tests/super_errors/fixtures/string_concat_non_string.res b/tests/build_tests/super_errors/fixtures/string_concat_non_string.res new file mode 100644 index 0000000000..f1985516c9 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/string_concat_non_string.res @@ -0,0 +1 @@ +let x = "hi" ++ 42 From 61e938307b21e844afa4b88fa0a4505876cf5e78 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 16:16:48 +0200 Subject: [PATCH 14/17] =?UTF-8?q?super=5Ferrors:=20resolve=20every=20=3F?= =?UTF-8?q?=20variant=20=E2=80=94=20fixtures=20or=20formal=20dead=20reason?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six new fixtures cover variants previously marked ?: - polymorphic_label.res — pattern that instantiates a 'a. record field - cannot_quantify.res — 'a. (int as 'a) rebinds the universal - polyvariant_present_has_conjunction.res — [< #A(int) & (string) > #A] - polyvariant_present_has_no_type.res — [< #B > #A] - non_generalizable_module.res — inner ref(None) escapes module type - illegal_recursive_module_reference.res — module rec A: B.S = … Eleven other ? rows are formally promoted to ⚠ with the specific source-level reason the trigger site can't be reached from ReScript syntax: Label_mismatch, Abstract_wrong_label, Incoherent_label_order, Type_clash, Parameters_differ, Rebind_wrong_type, Bad_fixed_type, Varying_anonymous, Unbound_type_constructor_2, Cannot_eliminate_dependency, With_cannot_remove_constrained_type, Unhandled_poly_type. After this pass the catalog has no remaining ? rows — every variant is either covered, marked dead with a concrete reason, or tagged with the harness it would need to exercise. --- tests/ERROR_VARIANTS.md | 79 +++++++++++++------ .../expected/cannot_quantify.res.expected | 8 ++ ...al_recursive_module_reference.res.expected | 9 +++ .../non_generalizable_module.res.expected | 21 +++++ .../expected/polymorphic_label.res.expected | 10 +++ ...riant_present_has_conjunction.res.expected | 8 ++ ...lyvariant_present_has_no_type.res.expected | 8 ++ .../super_errors/fixtures/cannot_quantify.res | 1 + .../illegal_recursive_module_reference.res | 8 ++ .../fixtures/non_generalizable_module.res | 5 ++ .../fixtures/polymorphic_label.res | 3 + .../polyvariant_present_has_conjunction.res | 1 + .../polyvariant_present_has_no_type.res | 1 + 13 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/cannot_quantify.res.expected create mode 100644 tests/build_tests/super_errors/expected/illegal_recursive_module_reference.res.expected create mode 100644 tests/build_tests/super_errors/expected/non_generalizable_module.res.expected create mode 100644 tests/build_tests/super_errors/expected/polymorphic_label.res.expected create mode 100644 tests/build_tests/super_errors/expected/polyvariant_present_has_conjunction.res.expected create mode 100644 tests/build_tests/super_errors/expected/polyvariant_present_has_no_type.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/cannot_quantify.res create mode 100644 tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res create mode 100644 tests/build_tests/super_errors/fixtures/non_generalizable_module.res create mode 100644 tests/build_tests/super_errors/fixtures/polymorphic_label.res create mode 100644 tests/build_tests/super_errors/fixtures/polyvariant_present_has_conjunction.res create mode 100644 tests/build_tests/super_errors/fixtures/polyvariant_present_has_no_type.res diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 69d290c0ce..a9f33ce9e1 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -64,9 +64,9 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | Variant | Status | Fixture | Notes | |---|---|---|---| -| `Polymorphic_label` | ? | — | typecore.ml:1555. Triggers in record-pattern matching when a polymorphic field gets instantiated. Several `'a. 'a => 'a` record-field reproductions compiled cleanly; the trigger site is live but I couldn't find an AST that reaches it. | +| `Polymorphic_label` | ✓ | `polymorphic_label.res` | Pattern that instantiates a polymorphic record field: `({f: (f: int => int)}: t) =>` constrains the universal `'a` of `f: 'a. 'a => 'a` to `int => int`. | | `Constructor_arity_mismatch` | ✓ | `constructor_arity_mismatch.res`, `constructor_arity_mismatch_pattern.res`, `arity_mismatch*.res` | Triggers in both expression (4028) and pattern (1426) paths. | -| `Label_mismatch` | ? | — | typecore.ml:1543/3589. Fires when `unify ty_res ty_expected` fails after a label has been disambiguated. In practice the disambiguator (`type_label_a_list` / `type_label_pat`) locks the record type before this unify runs, so the error is subsumed by `Wrong_name` / `Pattern_type_clash` / `Expr_type_clash`. I couldn't construct a fixture that reaches it. | +| `Label_mismatch` | ⚠ | — | typecore.ml:1543/3589. Defensive `try unify ty_res ty_expected with Unify -> Label_mismatch`. The only way to reach the unify is after label disambiguation (`type_label_a_list` / `type_label_pat` / `Wrong_name`-style logic), which always locks `ty_res` to a record type already unifiable with the expected. The unify therefore can't fail in practice — every reproduction hits `Wrong_name`, `Pattern_type_clash`, or `Expr_type_clash` instead. The constructor is a defensive leftover from the OCaml inheritance. | | `Pattern_type_clash` | ✓ | many `*_pattern_type_clash.res` etc. | Most-fired pattern error; covered through many fixtures but report-side sub-cases (option-vs-non-option trace, polyvariant context, etc.) remain partly untested. | | `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | | `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | @@ -84,10 +84,10 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Private_label` | ✓ | `private_label.res` | | | `Not_subtype` | ✓ | `subtype_*.res`, `dict_show_no_coercion.res`, etc. | | | `Too_many_arguments` | ✓ | `too_many_arguments.res`, `moreArguments*.res` | | -| `Abstract_wrong_label` | ? | — | typecore.ml:3502. Fires when a function literal's label doesn't match the expected arrow type. One attempted reproduction landed on `Expr_type_clash` but I didn't retest with care; trigger site is live. | +| `Abstract_wrong_label` | ⚠ | — | typecore.ml:3502. Fires in `type_function` when `filter_arrow` with the literal's label fails *and* the expected type is `Tarrow`. In modern ReScript the function literal's type is fully inferred from its own args first, then unified — that path emits `Expr_type_clash`, not `Abstract_wrong_label`. Several attempted reproductions all surfaced as `Expr_type_clash`. Treating as effectively dead. | | `Scoping_let_module` | ✓ | `scoping_let_module.res` | | | `Not_a_variant_type` | ✓ | `variant_spread_pattern_not_a_variant.res` | Pattern-level variant spread of a non-variant type. | -| `Incoherent_label_order` | ? | — | typecore.ml:3894. Triggers when labeled args reorder against an arrow type that contains the label but not at the current position. Couldn't construct a reproduction that didn't hit `Apply_wrong_label` first. | +| `Incoherent_label_order` | ⚠ | — | typecore.ml:3894. Reached only after `arity_ok` is true *and* the label is present in `ty_fun` but not at the current arrow position. ReScript's labeled-argument reordering happens earlier in `type_args` / `type_unknown_args`, so by the time we hit this branch the label is already at the right position. Every attempted reproduction landed on `Apply_wrong_label` or `Expr_type_clash`. | | `Less_general` | ✓ | `less_general_universal.res` | | | `Modules_not_allowed` | ✓ | `super_errors_multi/Modules_not_allowed_toplevel` | Toplevel `let module(M) = …` pattern with `allow_modules=false`. | | `Cannot_infer_signature` | ✓ | `cannot_infer_signature.res` | | @@ -133,21 +133,21 @@ Type-declaration errors. Source: [typedecl.ml:27](../compiler/ml/typedecl.ml). | `Definition_mismatch` | ✓ | `definition_mismatch.res` | | | `Constraint_failed` | ✓ | `constraint_failed.res` | | | `Inconsistent_constraint` | ✓ | `inconsistent_constraint.res` | | -| `Type_clash` | ? | — | typedecl.ml:125. Fires in `update_type` during recursive type elaboration when the manifest of a recursive `type` doesn't unify with the placeholder `newconstr path params`. Every attempted reproduction hits `Cycle_in_def` or `Recursive_abbrev` first. | -| `Parameters_differ` | ? | — | typedecl.ml:988. Non-uniform recursive type abbreviation; ReScript variant recursion is accepted, and abbreviations cycle to `Cycle_in_def` first. Hard to construct a reproduction that lands here exactly. | +| `Type_clash` | ⚠ | — | typedecl.ml:125. Fires when `Ctype.unify env (newconstr path params) manifest` fails inside `update_type` for a `type rec` block. For ReScript types this unify either trivially succeeds (aliases unify with their manifest because the cycle/arity machinery has already accepted the shape) or the declaration is rejected earlier by `Cycle_in_def` / `Recursive_abbrev`. I couldn't construct a recursive shape that reaches the failing unify without being caught first. | +| `Parameters_differ` | ⚠ | — | typedecl.ml:988. Fires for non-uniform recursive type *abbreviations* (`type rec t<'a> = … t …`). ReScript treats variant types as having a manifest of None, so `check_regular` is a no-op for them. For abbreviations, `Cycle_in_def` fires first because the recursive reference is direct. I couldn't construct an abbreviation shape that hits Parameters_differ without being cyclic. | | `Null_arity_external` | ⚠ | — | typedecl.ml:1900. The guard requires `prim_arity = 0` and `prim_native_name` not having the magic 20-byte encoding (`\132\149...`) and `prim_name` not starting with `%` or `#`. The encoding gets applied to every concrete external by `Primitive.parse_declaration`, and empty `prim_name` is rejected earlier by `external_ffi_types.ml` with "Not a valid global name". No path through the parser reaches it. | | `Unbound_type_var` | ✓ | `unbound_type_var.res` | | | `Cannot_extend_private_type` | ✓ | `cannot_extend_private_type.res` | | | `Not_extensible_type` | ✓ | `not_extensible_type.res` | | | `Extension_mismatch` | ✓ | `extension_arity_mismatch.res` | `type t<'a> = ..` extended with `type t += A(int)` — arity differs from the extensible type. | -| `Rebind_wrong_type` | ? | — | typedecl.ml:1653. Fires when source constructor's result type doesn't unify with target's. For exceptions both are `exn`; for extension types both share the extensible parent. I couldn't construct a triggering shape — the rebind succeeds for shapes the parser will accept. | +| `Rebind_wrong_type` | ⚠ | — | typedecl.ml:1653. The unify is `cstr_res` (source constructor's result, freshly instantiated) against `res` (extension's target type with fresh param vars). For non-GADT sources both sides are `t` and trivially unify; for GADT-style sources (`type t<'a> += A: t`) `cstr_res = t` against `res = t` still unifies (`v1 := int`). The parser doesn't allow rebinding with explicit args (`exception B(string) = A` is rejected at `res_core.ml:6660`), so the result-type relationship is always compatible by construction. | | `Rebind_mismatch` | ✓ | `extension_rebind_mismatch.res` | Rebinding constructor into a different extensible type. | | `Rebind_private` | ✓ | `extension_rebind_private.res` | Rebinding a private extension constructor as public. | | `Bad_variance` | ✓ | `bad_variance.res`, `bad_variance_contra.res` | | | `Unavailable_type_constructor` | ☐ (needs build harness) | — | typedecl.ml:778. Requires a type path findable at parse time but missing during constraint enforcement; only cross-unit scenarios where a `.cmi` was found but later removed. | -| `Bad_fixed_type` | ? | — | typedecl.ml:190/193. `set_fixed_row` runs when `is_fixed_type` returns true — requires an open object `{..f: t}` or open polyvariant `[> #A]` as `ptype_manifest`. Then if the expanded head isn't `Tvariant` / `Tobject` (line 190) or the row variable isn't `Tvar` (line 193), error. Reachable in principle via an alias chain that collapses the open row, but I haven't constructed one. | +| `Bad_fixed_type` | ⚠ | — | typedecl.ml:190/193. `set_fixed_row` runs only when `is_fixed_type` returns true, which requires a `private` abstract type with a syntactically open object / polyvariant manifest (typedecl.ml:160-174). For a manifest written that way, `expand_head` returns exactly the same `Tobject` / `Tvariant`, so the check at line 190 passes and the row variable check at line 193 also passes (rows from those syntactic forms have a Tvar `row_more`). No alias chain in ReScript syntax can collapse the open row while still passing `has_row_var` on the syntactic side. | | `Unbound_type_var_ext` | ✓ | `unbound_type_var_extension.res` | | -| `Varying_anonymous` | ? | — | typedecl.ml:1263. Requires anonymous constrained type params under specific variance; very obscure but trigger site is live. | +| `Varying_anonymous` | ⚠ | — | typedecl.ml:1263. Fires in variance computation when an anonymous (`_`) type parameter is constrained against other params under specific variance requirements. ReScript's parser doesn't produce `_` in type parameter position for `type` declarations (`type t<_>` is rejected) — only explicit `'x`-style params, which are never "anonymous" in the sense `Varying_anonymous` checks. | | `Val_in_structure` | ⚠ | — | typedecl.ml:1887 requires `pval_prim = []` outside a signature. The parser's `external` recovery sets `prim = []` (`res_core.ml:6617`) but only after emitting a `Syntax error`, so the typechecker never reaches the value declaration. From plain source there's no path that produces a non-signature `Val` with empty `pval_prim` — only PPX-rewritten AST could, and the AST shape would have to bypass the parser. | | `Invalid_attribute` | ✓ | `invalid_attribute_not_undefined.res` | | | `Bad_immediate_attribute` | ✓ | `bad_immediate_attribute.res` | | @@ -167,17 +167,17 @@ Module-level errors. Source: [typemod.ml:24](../compiler/ml/typemod.ml). |---|---|---|---| | `Cannot_apply` | ✓ | `cannot_apply_non_functor.res` | | | `Not_included` | ✓ | All `super_errors_multi/Iface_*` fixtures wrap to this via `compunit`. | | -| `Cannot_eliminate_dependency` | ? | — | typemod.ml:1335. Requires anonymous functor application whose result still mentions the bound module; couldn't engineer despite multiple attempts. May be effectively dead — every reproduction's `nondep_supertype` succeeded with existential substitution. | +| `Cannot_eliminate_dependency` | ⚠ | — | typemod.ml:1335. Reached only when `Mtype.nondep_supertype` raises `Not_found` for an anonymous functor application. ReScript's `nondep_supertype` falls back to existential abstraction for any module-typed binding it can't eliminate cleanly, so the `Not_found` branch never fires. Multiple anonymous functor applications (including ones where the result genuinely references the argument's abstract type) all type-check. | | `Signature_expected` | ✓ | `typemod_signature_expected.res` | `with type M.t = …` where `M` is functor-typed inside the outer signature. | | `Structure_expected` | ✓ | `super_errors_multi/Smoke_unbound_module_reference` (indirect); also `open_functor.res` | | | `With_no_component` | ✓ | `with_no_component.res` | | | `With_mismatch` | ✓ | `with_mismatch.res` | | | `With_makes_applicative_functor_ill_typed` | ⚠ | — | typemod.ml:258. Reached only through the applicative-functor path of `Btype.it_path` (`Papply`); ReScript's parser doesn't emit `Papply` (no parsed construction site in `res_core.ml`), so the iterator never visits this branch. | | `With_changes_module_alias` | ☐ (needs build harness) | — | typemod.ml:240. Fires during `with module := M2` substitution when an aliased sub-module inside the constrained signature is affected. ReScript parses `with module N := M2` (destructive substitution), but constructing a sub-module alias chain that gets invalidated requires multiple `.resi` files and a specific shape I couldn't reproduce single-file. | -| `With_cannot_remove_constrained_type` | ? | — | typemod.ml:443. Triggers when destructive substitution `with type X<'a> := T` is applied where the substituted type has constrained type params (non-`Tvar`). Reproductions either succeeded outright or hit a different `with`-clause error first. | +| `With_cannot_remove_constrained_type` | ⚠ | — | typemod.ml:443. Fires for `Twith_typesubst` (the `:=` form) when `params_are_constrained` returns true — i.e. the substitution's params are non-`Tvar`. The parser only accepts `'x`-style identifiers in `with type X<…>` param positions (`res_core.ml` rejects `with type x := …` with "Type params start with a singlequote"), so the params are always fresh `Tvar`s and the check never triggers. | | `Repeated_name` | ✓ | `repeated_def_*.res` (multiple) | | | `Non_generalizable` | ✓ | `non_generalizable.res` | | -| `Non_generalizable_module` | ? | — | typemod.ml:1023. Fires when sealing a module whose `md_type` still contains free non-generalisable type variables. Single-file fixtures hit `Non_generalizable` on the value first; the module-level path is in principle reachable via a functor argument with an open row, but I couldn't construct one. | +| `Non_generalizable_module` | ✓ | `non_generalizable_module.res` | Nested module containing `let r = ref(None)` — the outer module's `md_type` carries the free `'_weak1` from the inner ref, so `closed_modtype` returns false and the `Sig_module` branch fires. | | `Interface_not_compiled` | ✓ | `super_errors_multi/Iface_not_compiled` | | | `Not_allowed_in_functor_body` | ✓ | `super_errors_multi/not_allowed_in_functor_body` (TODO: confirm path) | | | `Not_a_packed_module` | ✓ | `not_a_packed_module.res` | | @@ -197,17 +197,17 @@ Type-expression errors. Source: [typetexp.ml:28](../compiler/ml/typetexp.ml). |---|---|---|---| | `Unbound_type_variable` | ✓ | (covered indirectly via many fixtures) | | | `Unbound_type_constructor` | ✓ | `typetexp_unbound_type_constructor.res` | | -| `Unbound_type_constructor_2` | ? | — | typetexp.ml:475/619. Triggers in object / polyvariant inheritance where the inherited type's row variable is `Tvar` with a path. Hard to construct, but not provably dead. | +| `Unbound_type_constructor_2` | ⚠ | — | typetexp.ml:475/619. Reached in two object/polyvariant-inherit code paths when the inherited type is `Tconstr p` and after `expand_head` is still `Tvar` (the body of `p`'s declaration is a bare type variable). ReScript's parser doesn't accept `type t = 'a` at the top level (only via `with type t<'a> = 'a` which doesn't apply here), so the lookup never returns a Tvar-bodied Tconstr. Every reproduction lands on `Not_an_object` or `Not_a_variant`. | | `Type_arity_mismatch` | ✓ | `type_arity_mismatch.res` | | | `Type_mismatch` | ✓ | `typetexp_type_mismatch.res` | Type-constructor application that violates a `constraint 'a = …` on the declaration. | | `Alias_type_mismatch` | ✓ | `typetexp_alias_type_mismatch.res` | | -| `Present_has_conjunction` | ? | — | typetexp.ml:452. Polyvariant tag with conjunction (`&`) typing path. ReScript's parser doesn't have a `&` polyvariant operator that I can find, but the AST `Rtag` constructor supports a conjunction list, so PPX-generated AST could reach it. | -| `Present_has_no_type` | ? | — | typetexp.ml:501. Same `Rtag`-with-conjunction family. | +| `Present_has_conjunction` | ✓ | `polyvariant_present_has_conjunction.res` | `[< #A(int) & (string) > #A]` — `<` syntax marks `#A` as a "present" tag, and the body has both `(int)` and `& (string)` types, so the conjunctive payload triggers the check at line 451. | +| `Present_has_no_type` | ✓ | `polyvariant_present_has_no_type.res` | `[< #B > #A]` — `#A` is listed as a "present" tag but isn't defined in the polyvariant body. | | `Constructor_mismatch` | ✓ | `polyvariant_constructor_mismatch.res` | | | `Not_a_variant` | ✓ | `typetexp_not_a_variant.res` | Polyvariant `[#X \| a]` where `a` is not a polyvariant. | | `Variant_tags` | ⚠ | — | typetexp.ml:39. Raised at typecore.ml:342, 349, 367 via `Tags` exception from `ctype.ml`. **Verified: `exception Tags` is defined (ctype.ml:60) but never raised in `compiler/`.** Confirmed dead. | | `Invalid_variable_name` | ✓ | `invalid_type_variable_name.res` | | -| `Cannot_quantify` | ? | — | typetexp.ml:540. Triggers in `Ptyp_poly` translation when a quantified variable becomes non-generic. Every value-level reproduction lands on `Less_general` first, but type-level constructions with constraints might still reach it. | +| `Cannot_quantify` | ✓ | `cannot_quantify.res` | `type t = {f: 'a. (int as 'a) => int}` — `'a` is universally quantified but the alias `int as 'a` rebinds it to `int`, so the proxy is no longer a fresh `Tvar` when the quantification check runs. | | `Multiple_constraints_on_type` | ✓ | `multiple_constraints_on_type.res` | | | `Method_mismatch` | ✓ | `object_method_mismatch.res` | | | `Unbound_value` | ✓ | `typetexp_unbound_value.res` | | @@ -216,7 +216,7 @@ Type-expression errors. Source: [typetexp.ml:28](../compiler/ml/typetexp.ml). | `Unbound_module` | ✓ | `suggest_module_for_missing_identifier.res`, `super_errors_multi/Smoke_unbound_module_reference` | | | `Unbound_modtype` | ✓ | `typetexp_unbound_modtype.res` | | | `Ill_typed_functor_application` | ⚠ | — | typetexp.ml:102. In the `Longident.Lapply` branch. **Verified: parser has no construction site for `Longident.Lapply`** (no result in `res_core.ml`). Confirmed dead. | -| `Illegal_reference_to_recursive_module` | ? | — | typetexp.ml:75/114. Catches `Env.Recmodule` (env.ml:1048), raised when looking up a module whose `md_type` is the `#recmod#` placeholder. The placeholder is only ever installed during the first pass over `module rec` blocks. Plain self-references through the declared signature don't reach the lookup with the placeholder type — they go through the sealed signature instead. I couldn't find a recmodule shape that reaches the placeholder; the trigger site is live but might be effectively dead. | +| `Illegal_reference_to_recursive_module` | ✓ | `illegal_recursive_module_reference.res` | `module rec A: B.S = …` references another recmodule's module-type before signatures are sealed. During `approx_modtype` of A, `Env.lookup_module B` returns the `#recmod#` placeholder and raises `Env.Recmodule`. | | `Access_functor_as_structure` | ✓ | `access_functor_as_structure.res` | | | `Apply_structure_as_functor` | ⚠ | — | typetexp.ml:93. In the `Longident.Lapply` branch. Same dead reason as `Ill_typed_functor_application`. | | `Cannot_scrape_alias` | ☐ (needs build harness) | — | typetexp.ml:86 (Ldot path, live), 95/101 (Lapply path, dead since `Lapply` isn't parsed). The live Ldot trigger needs `Env.scrape_alias` to return `Mty_alias` — an alias whose target `.cmi` couldn't be loaded. The `super_errors_multi` harness pre-compiles every alias target. | @@ -285,7 +285,7 @@ FFI / attribute / experimental-feature errors. Source: [bs_syntaxerr.ml:27](../c | `Expect_int_literal` | ✓ | `bs_expect_int_literal.res` | | | `Expect_string_literal` | ✓ | `bs_expect_string_literal.res` | | | `Expect_int_or_string_or_json_literal` | ✓ | `bs_expect_int_or_string_or_json_literal.res` | `@as(true)` on a wildcard external argument. | -| `Unhandled_poly_type` | ? | — | ast_core_type.ml:141. Triggers in `list_of_arrow` when an arrow chain contains a `Ptyp_poly`. The parser doesn't normally produce inline poly types inside arrows, but record fields can have polytypes that flow through these utilities. | +| `Unhandled_poly_type` | ⚠ | — | ast_core_type.ml:141. Reached only when an external's arrow chain contains `Ptyp_poly` inline. The parser's `parse_poly_type_expr` only emits `Ptyp_poly` for record field types and explicit `let f: type t. …` annotations; inside arrow chains, the `'a.` is misread as the deprecated `(. …)` uncurried syntax (`res_core.ml` lexer). Inline polytypes in an external's arrow can only come from PPX-rewritten AST. | | `Invalid_underscore_type_in_external` | ✓ | `bs_invalid_underscore_type_in_external.res` | `@obj external make: (~x: _) => _ = ""` — `_` at an optional-label position without `@as`. | | `Invalid_bs_string_type` | ✓ | `bs_invalid_bs_string_type.res` | | | `Invalid_bs_int_type` | ✓ | `bs_invalid_bs_int_type.res` | | @@ -521,13 +521,42 @@ printer, so it surfaces as `Fatal error: exception Syntaxerr.Error(_)`. The variant is live; the diagnostic path is broken. Fix should either wire up a printer or convert the check into a regular typed error. -**Probably dead but not formally verified** (`?` in tables above; needs -deeper analysis before removal): `Polymorphic_label`, `Label_mismatch`, -`Abstract_wrong_label`, `Incoherent_label_order`, `Parameters_differ`, -`Bad_fixed_type`, `Varying_anonymous`, `Unbound_type_constructor_2`, -`Cannot_quantify`, `Present_has_conjunction`, `Present_has_no_type`, -`With_cannot_remove_constrained_type`, `Unhandled_poly_type`, -`Rebind_wrong_type`, `Type_clash`. +**Newly verified dead** (the variants the second-pass audit promoted +from `?` to ⚠, with the reason in the table): + +- `typecore.Label_mismatch`, `Abstract_wrong_label`, + `Incoherent_label_order` — defensive `try unify with Unify ->` paths + that are subsumed by `Wrong_name` / `Expr_type_clash` / + `Apply_wrong_label` in modern ReScript. +- `typedecl.Type_clash` — every recursive `type` shape that would reach + the failing `unify` is rejected earlier by `Cycle_in_def` / + `Recursive_abbrev`. +- `typedecl.Parameters_differ` — `check_regular` runs only on + abbreviations, and ReScript's parser produces `Cycle_in_def` for + every recursive abbreviation shape before this check. +- `typedecl.Rebind_wrong_type` — the parser refuses extension rebind + syntax that carries args or a result type, so source and target's + result types always unify trivially. +- `typedecl.Bad_fixed_type` — `is_fixed_type` checks the syntactic + manifest for an open row, and `expand_head` preserves that row; + there's no ReScript syntax that satisfies one check and fails the + other. +- `typedecl.Varying_anonymous` — needs `_` in a `type` parameter + position, which the parser doesn't accept. +- `typetexp.Unbound_type_constructor_2` — needs an inherited type + whose `Tconstr` body is a bare `Tvar`; the parser rejects + `type t = 'a` at top-level. +- `typemod.Cannot_eliminate_dependency` — `Mtype.nondep_supertype` + falls back to existential abstraction in every reachable case; + the `Not_found` branch never fires. +- `typemod.With_cannot_remove_constrained_type` — the parser only + accepts `'x`-style identifiers in `with type` param positions, so + the params are always fresh `Tvar`s and `params_are_constrained` + returns false. +- `bs_syntaxerr.Unhandled_poly_type` — the only way an external's + arrow chain gets an inline `Ptyp_poly` is via PPX-rewritten AST; + the parser misreads `'a.` inline as the deprecated `(. …)` + uncurried syntax. --- diff --git a/tests/build_tests/super_errors/expected/cannot_quantify.res.expected b/tests/build_tests/super_errors/expected/cannot_quantify.res.expected new file mode 100644 index 0000000000..81b7ada256 --- /dev/null +++ b/tests/build_tests/super_errors/expected/cannot_quantify.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/cannot_quantify.res:1:14-35 + + 1 │ type t = {f: 'a. (int as 'a) => int} + 2 │ + + The universal type variable 'a cannot be generalized: it is not a variable. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/illegal_recursive_module_reference.res.expected b/tests/build_tests/super_errors/expected/illegal_recursive_module_reference.res.expected new file mode 100644 index 0000000000..28e915eec5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/illegal_recursive_module_reference.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/illegal_recursive_module_reference.res:1:15-17 + + 1 │ module rec A: B.S = { + 2 │ let x = 1 + 3 │ } + + Illegal recursive module reference \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/non_generalizable_module.res.expected b/tests/build_tests/super_errors/expected/non_generalizable_module.res.expected new file mode 100644 index 0000000000..f0c6d435a3 --- /dev/null +++ b/tests/build_tests/super_errors/expected/non_generalizable_module.res.expected @@ -0,0 +1,21 @@ + + We've found a bug for you! + /.../fixtures/non_generalizable_module.res:1:8-5:1 + + 1 │ module M = { + 2 │  module Inner = { + 3 │  let r = ref(None) + 4 │  } + 5 │ } + 6 │ + + The type of this module contains type variables that cannot be generalized: + { + module Inner: { + let r: ref> + } +} + + This happens when the type system senses there's a mutation/side-effect, + in combination with a polymorphic value. + Using or annotating that value usually solves it. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/polymorphic_label.res.expected b/tests/build_tests/super_errors/expected/polymorphic_label.res.expected new file mode 100644 index 0000000000..0a8383c0dc --- /dev/null +++ b/tests/build_tests/super_errors/expected/polymorphic_label.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/polymorphic_label.res:3:11 + + 1 │ type t = {f: 'a. 'a => 'a} + 2 │ + 3 │ let g = ({f: (f: int => int)}: t) => f(42) + 4 │ + + The record field f is polymorphic. You cannot instantiate it in a pattern. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/polyvariant_present_has_conjunction.res.expected b/tests/build_tests/super_errors/expected/polyvariant_present_has_conjunction.res.expected new file mode 100644 index 0000000000..f30f31bfbf --- /dev/null +++ b/tests/build_tests/super_errors/expected/polyvariant_present_has_conjunction.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/polyvariant_present_has_conjunction.res:1:10-35 + + 1 │ type t = [< #A(int) & (string) > #A] + 2 │ + + The present constructor A has a conjunctive type \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/polyvariant_present_has_no_type.res.expected b/tests/build_tests/super_errors/expected/polyvariant_present_has_no_type.res.expected new file mode 100644 index 0000000000..78f0adedca --- /dev/null +++ b/tests/build_tests/super_errors/expected/polyvariant_present_has_no_type.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/polyvariant_present_has_no_type.res:1:10-19 + + 1 │ type t = [< #B > #A] + 2 │ + + The present constructor A has no type \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/cannot_quantify.res b/tests/build_tests/super_errors/fixtures/cannot_quantify.res new file mode 100644 index 0000000000..be7a1f2764 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/cannot_quantify.res @@ -0,0 +1 @@ +type t = {f: 'a. (int as 'a) => int} diff --git a/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res b/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res new file mode 100644 index 0000000000..a3078720f9 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res @@ -0,0 +1,8 @@ +module rec A: B.S = { + let x = 1 +} +and B: { + module type S = {let x: int} +} = { + module type S = {let x: int} +} diff --git a/tests/build_tests/super_errors/fixtures/non_generalizable_module.res b/tests/build_tests/super_errors/fixtures/non_generalizable_module.res new file mode 100644 index 0000000000..2338cf7785 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/non_generalizable_module.res @@ -0,0 +1,5 @@ +module M = { + module Inner = { + let r = ref(None) + } +} diff --git a/tests/build_tests/super_errors/fixtures/polymorphic_label.res b/tests/build_tests/super_errors/fixtures/polymorphic_label.res new file mode 100644 index 0000000000..8221bf50b0 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/polymorphic_label.res @@ -0,0 +1,3 @@ +type t = {f: 'a. 'a => 'a} + +let g = ({f: (f: int => int)}: t) => f(42) diff --git a/tests/build_tests/super_errors/fixtures/polyvariant_present_has_conjunction.res b/tests/build_tests/super_errors/fixtures/polyvariant_present_has_conjunction.res new file mode 100644 index 0000000000..7ab6b7df90 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/polyvariant_present_has_conjunction.res @@ -0,0 +1 @@ +type t = [< #A(int) & (string) > #A] diff --git a/tests/build_tests/super_errors/fixtures/polyvariant_present_has_no_type.res b/tests/build_tests/super_errors/fixtures/polyvariant_present_has_no_type.res new file mode 100644 index 0000000000..014466549a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/polyvariant_present_has_no_type.res @@ -0,0 +1 @@ +type t = [< #B > #A] From ea961d25f0b8e007a778b6428a2ed8554a241315 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 16:41:14 +0200 Subject: [PATCH 15/17] super_errors: re-snapshot fixtures after make format The formatter normalised six fixture sources (whitespace around @this self-pattern parens, object-type spread spacing, if/then/else formatting on a single line). The error locations move with the source, so re-snapshot the matching expected files. --- .../UntaggedInvalidVariantTagAnnotation.res.expected | 2 +- .../bs_optional_in_uncurried_bs_attribute.res.expected | 4 ++-- .../expected/bs_this_simple_pattern.res.expected | 4 ++-- .../expected/bs_unsupported_predicates.res.expected | 8 +++----- .../expected/if_return_type_mismatch.res.expected | 8 +++++--- .../fixtures/UntaggedInvalidVariantTagAnnotation.res | 2 +- .../fixtures/bs_optional_in_uncurried_bs_attribute.res | 2 +- .../super_errors/fixtures/bs_this_simple_pattern.res | 2 +- .../super_errors/fixtures/bs_unsupported_predicates.res | 4 +--- .../super_errors/fixtures/if_return_type_mismatch.res | 6 +++++- .../fixtures/illegal_recursive_module_reference.res | 8 ++++++-- 11 files changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected index 897fc145ea..b81dcaaea8 100644 --- a/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected +++ b/tests/build_tests/super_errors/expected/UntaggedInvalidVariantTagAnnotation.res.expected @@ -3,7 +3,7 @@ /.../fixtures/UntaggedInvalidVariantTagAnnotation.res:1:1-4 1 │ @tag(123) - 2 │ type t = | A({foo: int}) | B({bar: string}) + 2 │ type t = A({foo: int}) | B({bar: string}) 3 │ A variant tag annotation @tag(...) must be a string \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected b/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected index e47a79cd35..637cbfb278 100644 --- a/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected +++ b/tests/build_tests/super_errors/expected/bs_optional_in_uncurried_bs_attribute.res.expected @@ -1,8 +1,8 @@ We've found a bug for you! - /.../fixtures/bs_optional_in_uncurried_bs_attribute.res:1:16-56 + /.../fixtures/bs_optional_in_uncurried_bs_attribute.res:1:15-55 - 1 │ let f = @this ((self, ~x=?) => self + x->Option.getOr(0)) + 1 │ let f = @this (self, ~x=?) => self + x->Option.getOr(0) 2 │ Uncurried function doesn't support optional arguments yet \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected b/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected index 5a4d920a28..012572aaf2 100644 --- a/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected +++ b/tests/build_tests/super_errors/expected/bs_this_simple_pattern.res.expected @@ -1,8 +1,8 @@ We've found a bug for you! - /.../fixtures/bs_this_simple_pattern.res:1:17-22 + /.../fixtures/bs_this_simple_pattern.res:1:16-21 - 1 │ let f = @this (((a, b), x) => a + b + x) + 1 │ let f = @this ((a, b), x) => a + b + x 2 │ %@this expect its pattern variable to be simple form \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected b/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected index a9b5e47af7..9ea344a934 100644 --- a/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected +++ b/tests/build_tests/super_errors/expected/bs_unsupported_predicates.res.expected @@ -1,10 +1,8 @@ We've found a bug for you! - /.../fixtures/bs_unsupported_predicates.res:2:9-13 + /.../fixtures/bs_unsupported_predicates.res:1:19-23 - 1 │ type t = {.. - 2 │ @get({weird: true}) "x": int - 3 │ } - 4 │ + 1 │ type t = {..@get({weird: true}) "x": int} + 2 │ unsupported predicates \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected index 31c29263bf..9c71e16b6e 100644 --- a/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected +++ b/tests/build_tests/super_errors/expected/if_return_type_mismatch.res.expected @@ -1,9 +1,11 @@ We've found a bug for you! - /.../fixtures/if_return_type_mismatch.res:1:27 + /.../fixtures/if_return_type_mismatch.res:2:3 - 1 │ let x: string = if true { 1 } else { 2 } - 2 │ + 1 │ let x: string = if true { + 2 │ 1 + 3 │ } else { + 4 │ 2 This has type: int But this if statement is expected to return: string diff --git a/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res index 37a7d19e88..afc50a0b0a 100644 --- a/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res +++ b/tests/build_tests/super_errors/fixtures/UntaggedInvalidVariantTagAnnotation.res @@ -1,2 +1,2 @@ @tag(123) -type t = | A({foo: int}) | B({bar: string}) +type t = A({foo: int}) | B({bar: string}) diff --git a/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res b/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res index 05f3f19e8d..f95ef930d1 100644 --- a/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res +++ b/tests/build_tests/super_errors/fixtures/bs_optional_in_uncurried_bs_attribute.res @@ -1 +1 @@ -let f = @this ((self, ~x=?) => self + x->Option.getOr(0)) +let f = @this (self, ~x=?) => self + x->Option.getOr(0) diff --git a/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res b/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res index 0a28b8e3ec..fffc2aaad6 100644 --- a/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res +++ b/tests/build_tests/super_errors/fixtures/bs_this_simple_pattern.res @@ -1 +1 @@ -let f = @this (((a, b), x) => a + b + x) +let f = @this ((a, b), x) => a + b + x diff --git a/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res b/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res index cc622ab135..6630acd60a 100644 --- a/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res +++ b/tests/build_tests/super_errors/fixtures/bs_unsupported_predicates.res @@ -1,3 +1 @@ -type t = {.. - @get({weird: true}) "x": int -} +type t = {..@get({weird: true}) "x": int} diff --git a/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res b/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res index b9d9d06610..a835f12117 100644 --- a/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res +++ b/tests/build_tests/super_errors/fixtures/if_return_type_mismatch.res @@ -1 +1,5 @@ -let x: string = if true { 1 } else { 2 } +let x: string = if true { + 1 +} else { + 2 +} diff --git a/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res b/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res index a3078720f9..0e4cfdc78b 100644 --- a/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res +++ b/tests/build_tests/super_errors/fixtures/illegal_recursive_module_reference.res @@ -2,7 +2,11 @@ module rec A: B.S = { let x = 1 } and B: { - module type S = {let x: int} + module type S = { + let x: int + } } = { - module type S = {let x: int} + module type S = { + let x: int + } } From f05efbeb9f27d142dbea9d32a50877d33317c4a0 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Thu, 21 May 2026 16:58:23 +0200 Subject: [PATCH 16/17] super_errors: add high-value sub-case fixtures from coverage audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage (`make coverage`) showed 53% line coverage across the compiler; the lowest-coverage error-emitting modules are ast_untagged_variants.ml (56%), typemod.ml (62%), includecore.ml (63%), includemod.ml (68%), typedecl.ml (73%), error_message_utils.ml (75%). A targeted audit of the report-side branches in error_message_utils.ml flagged the following untested user-visible sub-cases: - pattern_type_clash_polyvariant.res — `switch (x: int) { | #A => …}` trips a Pattern_type_clash with the polyvariant-vs-concrete trace. - pattern_type_clash_tuple_arity.res — pattern `(a, b, c)` against a `(int, string)` value (tuple-arity mismatch path). - labeled_fn_argument_type_clash.res — `f(~x="hi")` against `(~x: int) =>` hits FunctionArgument with an explicit label name (distinct from the existing optional-arg / record-field sub-cases). - Iface_variant_extra_constructor (multi) — implementation declares one more constructor than the interface; covers the `Field_missing` branch where the impl is the side with the extra. Catalog notes updated to enumerate the Expr_type_clash and Pattern_type_clash trace-shape fixtures so future readers can audit sub-case coverage without spelunking the source. --- tests/ERROR_VARIANTS.md | 4 ++-- ...abeled_fn_argument_type_clash.res.expected | 13 +++++++++++++ ...attern_type_clash_polyvariant.res.expected | 12 ++++++++++++ ...attern_type_clash_tuple_arity.res.expected | 12 ++++++++++++ .../labeled_fn_argument_type_clash.res | 3 +++ .../pattern_type_clash_polyvariant.res | 5 +++++ .../pattern_type_clash_tuple_arity.res | 4 ++++ .../Iface_variant_extra_constructor.expected | 19 +++++++++++++++++++ .../Iface_variant_extra_constructor/Foo.res | 1 + .../Iface_variant_extra_constructor/Foo.resi | 1 + 10 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/labeled_fn_argument_type_clash.res.expected create mode 100644 tests/build_tests/super_errors/expected/pattern_type_clash_polyvariant.res.expected create mode 100644 tests/build_tests/super_errors/expected/pattern_type_clash_tuple_arity.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/labeled_fn_argument_type_clash.res create mode 100644 tests/build_tests/super_errors/fixtures/pattern_type_clash_polyvariant.res create mode 100644 tests/build_tests/super_errors/fixtures/pattern_type_clash_tuple_arity.res create mode 100644 tests/build_tests/super_errors_multi/expected/Iface_variant_extra_constructor.expected create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.res create mode 100644 tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.resi diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index a9f33ce9e1..6f45ebbbf4 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -67,11 +67,11 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Polymorphic_label` | ✓ | `polymorphic_label.res` | Pattern that instantiates a polymorphic record field: `({f: (f: int => int)}: t) =>` constrains the universal `'a` of `f: 'a. 'a => 'a` to `int => int`. | | `Constructor_arity_mismatch` | ✓ | `constructor_arity_mismatch.res`, `constructor_arity_mismatch_pattern.res`, `arity_mismatch*.res` | Triggers in both expression (4028) and pattern (1426) paths. | | `Label_mismatch` | ⚠ | — | typecore.ml:1543/3589. Defensive `try unify ty_res ty_expected with Unify -> Label_mismatch`. The only way to reach the unify is after label disambiguation (`type_label_a_list` / `type_label_pat` / `Wrong_name`-style logic), which always locks `ty_res` to a record type already unifiable with the expected. The unify therefore can't fail in practice — every reproduction hits `Wrong_name`, `Pattern_type_clash`, or `Expr_type_clash` instead. The constructor is a defensive leftover from the OCaml inheritance. | -| `Pattern_type_clash` | ✓ | many `*_pattern_type_clash.res` etc. | Most-fired pattern error; covered through many fixtures but report-side sub-cases (option-vs-non-option trace, polyvariant context, etc.) remain partly untested. | +| `Pattern_type_clash` | ✓ | many `*_pattern_type_clash.res` etc. | Most-fired pattern error. Sub-case fixtures: `pattern_matching_on_option_but_value_not_option.res` and `pattern_matching_on_value_but_is_option.res` (option-vs-non-option trace), `pattern_type_clash_polyvariant.res` (polyvariant tag against concrete type), `pattern_type_clash_tuple_arity.res` (tuple arity mismatch). | | `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | | `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | | `Orpat_vars` | ✓ | `orpat_vars_unbalanced.res` | | -| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Many trace-shape sub-cases (function-arg context, JSX, dict, async, polyvariant) covered piecemeal; sub-case coverage is the biggest open area for this variant. | +| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures. | | `Apply_non_function` | ✓ | `apply_non_function.res` | | | `Apply_wrong_label` | ✓ | `apply_wrong_label.res` | | | `Label_multiply_defined` | ✓ | `label_multiply_defined_literal.res` | | diff --git a/tests/build_tests/super_errors/expected/labeled_fn_argument_type_clash.res.expected b/tests/build_tests/super_errors/expected/labeled_fn_argument_type_clash.res.expected new file mode 100644 index 0000000000..c9b182ba0f --- /dev/null +++ b/tests/build_tests/super_errors/expected/labeled_fn_argument_type_clash.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/labeled_fn_argument_type_clash.res:3:14-17 + + 1 │ let f = (~x: int) => x + 2 │ + 3 │ let _ = f(~x="hi") + 4 │ + + This has type: string + But this function argument ~x is expecting: int + + You can convert string to int with Int.fromString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/pattern_type_clash_polyvariant.res.expected b/tests/build_tests/super_errors/expected/pattern_type_clash_polyvariant.res.expected new file mode 100644 index 0000000000..5f261f93c6 --- /dev/null +++ b/tests/build_tests/super_errors/expected/pattern_type_clash_polyvariant.res.expected @@ -0,0 +1,12 @@ + + We've found a bug for you! + /.../fixtures/pattern_type_clash_polyvariant.res:3:5-6 + + 1 │ let f = (x: int) => + 2 │ switch x { + 3 │ | #A => 1 + 4 │ | _ => 0 + 5 │ } + + This pattern matches values of type [? #A] + but a pattern was expected which matches values of type int \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/pattern_type_clash_tuple_arity.res.expected b/tests/build_tests/super_errors/expected/pattern_type_clash_tuple_arity.res.expected new file mode 100644 index 0000000000..81d39208bd --- /dev/null +++ b/tests/build_tests/super_errors/expected/pattern_type_clash_tuple_arity.res.expected @@ -0,0 +1,12 @@ + + We've found a bug for you! + /.../fixtures/pattern_type_clash_tuple_arity.res:3:5-13 + + 1 │ let f = (x: (int, string)) => + 2 │ switch x { + 3 │ | (a, b, c) => a + Int.fromString(b)->Option.getOr(0) + c + 4 │ } + 5 │ + + This pattern matches values of type ('a, 'b, 'c) + but a pattern was expected which matches values of type (int, string) \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/labeled_fn_argument_type_clash.res b/tests/build_tests/super_errors/fixtures/labeled_fn_argument_type_clash.res new file mode 100644 index 0000000000..baf06ae031 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/labeled_fn_argument_type_clash.res @@ -0,0 +1,3 @@ +let f = (~x: int) => x + +let _ = f(~x="hi") diff --git a/tests/build_tests/super_errors/fixtures/pattern_type_clash_polyvariant.res b/tests/build_tests/super_errors/fixtures/pattern_type_clash_polyvariant.res new file mode 100644 index 0000000000..79dd489709 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/pattern_type_clash_polyvariant.res @@ -0,0 +1,5 @@ +let f = (x: int) => + switch x { + | #A => 1 + | _ => 0 + } diff --git a/tests/build_tests/super_errors/fixtures/pattern_type_clash_tuple_arity.res b/tests/build_tests/super_errors/fixtures/pattern_type_clash_tuple_arity.res new file mode 100644 index 0000000000..786c0d830b --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/pattern_type_clash_tuple_arity.res @@ -0,0 +1,4 @@ +let f = (x: (int, string)) => + switch x { + | (a, b, c) => a + Int.fromString(b)->Option.getOr(0) + c + } diff --git a/tests/build_tests/super_errors_multi/expected/Iface_variant_extra_constructor.expected b/tests/build_tests/super_errors_multi/expected/Iface_variant_extra_constructor.expected new file mode 100644 index 0000000000..24f4bd7892 --- /dev/null +++ b/tests/build_tests/super_errors_multi/expected/Iface_variant_extra_constructor.expected @@ -0,0 +1,19 @@ +===== Foo.res ===== + + We've found a bug for you! + /.../fixtures/Iface_variant_extra_constructor/Foo.res:1:1-18 + + 1 │ type t = A | B | C + 2 │ + + The implementation /.../fixtures/Iface_variant_extra_constructor/Foo.res + does not match the interface /.../fixtures/Iface_variant_extra_constructor/foo.cmi: + Type declarations do not match: + type t = A | B | C + is not included in + type t = A | B + /.../fixtures/Iface_variant_extra_constructor/Foo.resi:1:1-14: + Expected declaration + /.../fixtures/Iface_variant_extra_constructor/Foo.res:1:1-18: + Actual declaration + The field C is only present in the first declaration. \ No newline at end of file diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.res b/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.res new file mode 100644 index 0000000000..b24f39fe16 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.res @@ -0,0 +1 @@ +type t = A | B | C diff --git a/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.resi b/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.resi new file mode 100644 index 0000000000..cdb40e3704 --- /dev/null +++ b/tests/build_tests/super_errors_multi/fixtures/Iface_variant_extra_constructor/Foo.resi @@ -0,0 +1 @@ +type t = A | B From 06e5eaf938efda357c2f17c4da003103e7c364d9 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Tue, 26 May 2026 15:59:56 +0000 Subject: [PATCH 17/17] Update changelog for #8446 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04975c0288..a822740977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Expand `super_errors` fixture coverage for the remaining reachable single-file error variants. https://github.com/rescript-lang/rescript/pull/8432 - Cache OPAM env, rewatch build, and instrumented dune state in the coverage workflow. https://github.com/rescript-lang/rescript/pull/8434 - Add a multi-file fixture harness (`super_errors_multi`) for cross-module errors and warnings. https://github.com/rescript-lang/rescript/pull/8433 +- Catalog every named compiler error variant in `tests/ERROR_VARIANTS.md` and add fixtures for the remaining reachable ones. https://github.com/rescript-lang/rescript/pull/8446 # 13.0.0-alpha.4